From 99b516d087561f6691282cad7a4d685f26c5c505 Mon Sep 17 00:00:00 2001 From: Dayowe Date: Mon, 18 Aug 2025 15:33:44 +0200 Subject: [PATCH] Add range calculations for dog food amounts based on life stage - Implement range multipliers for different life stages (e.g., 1.6-1.8x for intact adults) - Display MER and food amounts as ranges (e.g., '2042-2298 cal/day') - Add CSS to prevent value wrapping with white-space: nowrap - Increase calculator max-width from 600px to 640px for better text layout - Based on veterinary RER multiplier ranges for more accurate feeding recommendations --- iframe.html | 161 ++++++++++++++++++++++++++++++---- src/css/main.css | 22 ++++- src/js/calculator.js | 139 +++++++++++++++++++++++++---- sundog-dog-food-calculator.js | 161 ++++++++++++++++++++++++++++++---- 4 files changed, 426 insertions(+), 57 deletions(-) diff --git a/iframe.html b/iframe.html index 983d804..2db4ceb 100644 --- a/iframe.html +++ b/iframe.html @@ -32,7 +32,7 @@ } .dog-calculator-container { - max-width: 600px; + max-width: 640px; margin: 0 auto; padding: 24px; box-sizing: border-box; @@ -213,6 +213,7 @@ justify-content: space-between; align-items: center; margin-bottom: 12px; + gap: 10px; /* Add gap between label and value */ } .dog-calculator-result-item:last-child { @@ -232,6 +233,7 @@ padding: 4px 12px; background: rgba(241, 154, 95, 0.15); border-radius: 4px; + white-space: nowrap; /* Prevent text from wrapping to multiple lines */ } .dog-calculator-collapsible { @@ -543,6 +545,24 @@ flex-direction: column; align-items: flex-start; } + + /* Stack result items vertically on small screens */ + .dog-calculator-result-item { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .dog-calculator-result-label { + margin-right: 0; + font-size: 0.9rem; + } + + .dog-calculator-result-value { + font-size: 1rem; + align-self: stretch; + text-align: center; + } } /* Dark theme - manual override */ @@ -2288,6 +2308,8 @@ const CALCULATOR_CONFIG = { class DogCalorieCalculator { constructor() { this.currentMER = 0; + this.currentMERMin = 0; // For range calculations + this.currentMERMax = 0; // For range calculations this.isImperial = false; this.theme = this.getThemeFromURL() || CALCULATOR_CONFIG.defaultTheme; this.scale = this.getScaleFromURL() || CALCULATOR_CONFIG.defaultScale; @@ -3335,6 +3357,25 @@ const CALCULATOR_CONFIG = { return rer * factor; } + // Get the range multipliers for each life stage + getLifeStageRange(factor) { + // Define ranges based on the reference image + const ranges = { + '3.0': { min: 3.0, max: 3.0 }, // Puppy 0-4 months (no range) + '2.0': { min: 2.0, max: 2.0 }, // Puppy 4m-adult OR Working light (no range for puppies) + '1.2': { min: 1.2, max: 1.4 }, // Adult inactive/obese + '1.6': { min: 1.4, max: 1.6 }, // Adult neutered/spayed + '1.8': { min: 1.6, max: 1.8 }, // Adult intact + '1.0': { min: 1.0, max: 1.0 }, // Weight loss (fixed) + '1.7': { min: 1.2, max: 1.8 }, // Weight gain (wide range) + '5.0': { min: 5.0, max: 5.0 }, // Working heavy (upper bound) + '1.1': { min: 1.1, max: 1.1 } // Senior (no range) + }; + + const key = factor.toFixed(1); + return ranges[key] || { min: factor, max: factor }; + } + validateInput(value, min = 0, isInteger = false) { const num = parseFloat(value); if (isNaN(num) || num < min) return false; @@ -3466,10 +3507,21 @@ const CALCULATOR_CONFIG = { const rer = this.calculateRER(weightKg); const mer = this.calculateMER(rer, factor); - this.currentMER = mer; + // Calculate range for MER + const range = this.getLifeStageRange(factor); + this.currentMERMin = this.calculateMER(rer, range.min); + this.currentMERMax = this.calculateMER(rer, range.max); + this.currentMER = mer; // Keep middle/selected value for compatibility rerValue.textContent = this.formatNumber(rer, 0) + ' cal/day'; - merValue.textContent = this.formatNumber(mer, 0) + ' cal/day'; + + // Show MER as range if applicable + if (range.min !== range.max) { + merValue.textContent = this.formatNumber(this.currentMERMin, 0) + '-' + + this.formatNumber(this.currentMERMax, 0) + ' cal/day'; + } else { + merValue.textContent = this.formatNumber(mer, 0) + ' cal/day'; + } calorieResults.style.display = 'block'; this.updateFoodCalculations(); @@ -3504,6 +3556,9 @@ const CALCULATOR_CONFIG = { updateFoodCalculations() { if (this.currentMER === 0) return; + // Check if we have a range + const hasRange = this.currentMERMin !== this.currentMERMax; + const daysInput = document.getElementById('days'); const unitSelect = document.getElementById('unit'); const dailyFoodResults = document.getElementById('dailyFoodResults'); @@ -3576,43 +3631,70 @@ const CALCULATOR_CONFIG = { if (energyPer100g && energyPer100g > 0.1 && fs.percentage > 0) { const dailyCaloriesForThisFood = (this.currentMER * fs.percentage) / 100; + // Calculate range values if applicable + const dailyCaloriesMin = hasRange ? (this.currentMERMin * fs.percentage) / 100 : dailyCaloriesForThisFood; + const dailyCaloriesMax = hasRange ? (this.currentMERMax * fs.percentage) / 100 : dailyCaloriesForThisFood; + let dailyGramsForThisFood; + let dailyGramsMin, dailyGramsMax; let dailyCupsForThisFood = null; + let dailyCupsMin, dailyCupsMax; // For kcal/cup, calculate cups directly from calories if (fs.energyUnit === 'kcalcup' && fs.energy) { const caloriesPerCup = parseFloat(fs.energy); dailyCupsForThisFood = dailyCaloriesForThisFood / caloriesPerCup; + dailyCupsMin = dailyCaloriesMin / caloriesPerCup; + dailyCupsMax = dailyCaloriesMax / caloriesPerCup; // We still need grams for total calculation, use approximation dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 100; - console.log('Cups calculation:', { - caloriesPerCup, - dailyCaloriesForThisFood, - dailyCupsForThisFood, - dailyGramsForThisFood - }); + dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100; + dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100; } else { // For other units, calculate grams normally dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 100; + dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100; + dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100; } // Calculate per-meal amounts if needed const displayGrams = this.showPerMeal ? dailyGramsForThisFood / this.mealsPerDay : dailyGramsForThisFood; + const displayGramsMin = this.showPerMeal ? dailyGramsMin / this.mealsPerDay : dailyGramsMin; + const displayGramsMax = this.showPerMeal ? dailyGramsMax / this.mealsPerDay : dailyGramsMax; + const displayCups = dailyCupsForThisFood !== null ? (this.showPerMeal ? dailyCupsForThisFood / this.mealsPerDay : dailyCupsForThisFood) : null; + const displayCupsMin = dailyCupsMin !== undefined ? + (this.showPerMeal ? dailyCupsMin / this.mealsPerDay : dailyCupsMin) : null; + const displayCupsMax = dailyCupsMax !== undefined ? + (this.showPerMeal ? dailyCupsMax / this.mealsPerDay : dailyCupsMax) : null; + const displayCalories = this.showPerMeal ? dailyCaloriesForThisFood / this.mealsPerDay : dailyCaloriesForThisFood; + const displayCaloriesMin = this.showPerMeal ? dailyCaloriesMin / this.mealsPerDay : dailyCaloriesMin; + const displayCaloriesMax = this.showPerMeal ? dailyCaloriesMax / this.mealsPerDay : dailyCaloriesMax; foodBreakdowns.push({ name: fs.name, percentage: fs.percentage, dailyGrams: dailyGramsForThisFood, + dailyGramsMin: dailyGramsMin, + dailyGramsMax: dailyGramsMax, displayGrams: displayGrams, + displayGramsMin: displayGramsMin, + displayGramsMax: displayGramsMax, dailyCups: dailyCupsForThisFood, + dailyCupsMin: dailyCupsMin, + dailyCupsMax: dailyCupsMax, displayCups: displayCups, + displayCupsMin: displayCupsMin, + displayCupsMax: displayCupsMax, calories: dailyCaloriesForThisFood, displayCalories: displayCalories, + displayCaloriesMin: displayCaloriesMin, + displayCaloriesMax: displayCaloriesMax, isLocked: fs.isLocked, hasEnergyContent: true, + hasRange: hasRange, foodSource: fs // Store reference for cups conversion }); @@ -3717,12 +3799,23 @@ const CALCULATOR_CONFIG = { if (unit === 'cups') { // For cups, use the pre-calculated cups value if available if (breakdown.displayCups !== null) { - valueContent = `${this.formatNumber(breakdown.displayCups, decimals)} ${unitLabel}${frequencySuffix}`; + if (breakdown.hasRange && breakdown.displayCupsMin !== breakdown.displayCupsMax) { + valueContent = `${this.formatNumber(breakdown.displayCupsMin, decimals)}-${this.formatNumber(breakdown.displayCupsMax, decimals)} ${unitLabel}${frequencySuffix}`; + } else { + valueContent = `${this.formatNumber(breakdown.displayCups, decimals)} ${unitLabel}${frequencySuffix}`; + } } else { valueContent = `N/A`; } } else { - valueContent = `${this.formatNumber(this.convertUnits(breakdown.displayGrams, unit), decimals)} ${unitLabel}${frequencySuffix}`; + // For other units (g, kg, oz, lb) + if (breakdown.hasRange && breakdown.displayGramsMin !== breakdown.displayGramsMax) { + const minConverted = this.convertUnits(breakdown.displayGramsMin, unit); + const maxConverted = this.convertUnits(breakdown.displayGramsMax, unit); + valueContent = `${this.formatNumber(minConverted, decimals)}-${this.formatNumber(maxConverted, decimals)} ${unitLabel}${frequencySuffix}`; + } else { + valueContent = `${this.formatNumber(this.convertUnits(breakdown.displayGrams, unit), decimals)} ${unitLabel}${frequencySuffix}`; + } } } else { valueContent = `⚠️`; @@ -3760,23 +3853,53 @@ const CALCULATOR_CONFIG = { if (validForCups) { // Calculate total cups using pre-calculated values let totalCups = 0; + let totalCupsMin = 0; + let totalCupsMax = 0; foodBreakdowns.forEach(breakdown => { if (breakdown.percentage > 0 && breakdown.displayCups !== null) { totalCups += breakdown.displayCups; + if (breakdown.hasRange) { + totalCupsMin += breakdown.displayCupsMin || breakdown.displayCups; + totalCupsMax += breakdown.displayCupsMax || breakdown.displayCups; + } else { + totalCupsMin += breakdown.displayCups; + totalCupsMax += breakdown.displayCups; + } } }); - console.log('Total cups display:', { - totalCups, - displayTotal, - foodBreakdowns: foodBreakdowns.map(b => ({ name: b.name, displayCups: b.displayCups })) - }); - totalDisplayText = this.formatNumber(totalCups, decimals) + ` ${unitLabel}${frequencySuffix}`; + + if (hasRange && totalCupsMin !== totalCupsMax) { + totalDisplayText = `${this.formatNumber(totalCupsMin, decimals)}-${this.formatNumber(totalCupsMax, decimals)} ${unitLabel}${frequencySuffix}`; + } else { + totalDisplayText = this.formatNumber(totalCups, decimals) + ` ${unitLabel}${frequencySuffix}`; + } } else { totalDisplayText = 'Mixed units - see breakdown'; } } else { - convertedTotal = this.convertUnits(displayTotal, unit); - totalDisplayText = this.formatNumber(convertedTotal, decimals) + ` ${unitLabel}${frequencySuffix}`; + // Calculate totals for ranges + if (hasRange) { + let totalGramsMin = 0; + let totalGramsMax = 0; + foodBreakdowns.forEach(breakdown => { + if (breakdown.percentage > 0 && breakdown.hasEnergyContent) { + totalGramsMin += breakdown.displayGramsMin || breakdown.displayGrams; + totalGramsMax += breakdown.displayGramsMax || breakdown.displayGrams; + } + }); + + const convertedMin = this.convertUnits(totalGramsMin, unit); + const convertedMax = this.convertUnits(totalGramsMax, unit); + + if (totalGramsMin !== totalGramsMax) { + totalDisplayText = `${this.formatNumber(convertedMin, decimals)}-${this.formatNumber(convertedMax, decimals)} ${unitLabel}${frequencySuffix}`; + } else { + totalDisplayText = this.formatNumber(convertedMin, decimals) + ` ${unitLabel}${frequencySuffix}`; + } + } else { + convertedTotal = this.convertUnits(displayTotal, unit); + totalDisplayText = this.formatNumber(convertedTotal, decimals) + ` ${unitLabel}${frequencySuffix}`; + } } dailyFoodValue.textContent = totalDisplayText; diff --git a/src/css/main.css b/src/css/main.css index ada69e7..d08276a 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -22,7 +22,7 @@ } .dog-calculator-container { - max-width: 600px; + max-width: 640px; margin: 0 auto; padding: 24px; box-sizing: border-box; @@ -203,6 +203,7 @@ justify-content: space-between; align-items: center; margin-bottom: 12px; + gap: 10px; /* Add gap between label and value */ } .dog-calculator-result-item:last-child { @@ -222,6 +223,7 @@ padding: 4px 12px; background: rgba(241, 154, 95, 0.15); border-radius: 4px; + white-space: nowrap; /* Prevent text from wrapping to multiple lines */ } .dog-calculator-collapsible { @@ -533,6 +535,24 @@ flex-direction: column; align-items: flex-start; } + + /* Stack result items vertically on small screens */ + .dog-calculator-result-item { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .dog-calculator-result-label { + margin-right: 0; + font-size: 0.9rem; + } + + .dog-calculator-result-value { + font-size: 1rem; + align-self: stretch; + text-align: center; + } } /* Dark theme - manual override */ diff --git a/src/js/calculator.js b/src/js/calculator.js index 5804985..66edde8 100644 --- a/src/js/calculator.js +++ b/src/js/calculator.js @@ -6,6 +6,8 @@ class DogCalorieCalculator { constructor() { this.currentMER = 0; + this.currentMERMin = 0; // For range calculations + this.currentMERMax = 0; // For range calculations this.isImperial = false; this.theme = this.getThemeFromURL() || CALCULATOR_CONFIG.defaultTheme; this.scale = this.getScaleFromURL() || CALCULATOR_CONFIG.defaultScale; @@ -1053,6 +1055,25 @@ return rer * factor; } + // Get the range multipliers for each life stage + getLifeStageRange(factor) { + // Define ranges based on the reference image + const ranges = { + '3.0': { min: 3.0, max: 3.0 }, // Puppy 0-4 months (no range) + '2.0': { min: 2.0, max: 2.0 }, // Puppy 4m-adult OR Working light (no range for puppies) + '1.2': { min: 1.2, max: 1.4 }, // Adult inactive/obese + '1.6': { min: 1.4, max: 1.6 }, // Adult neutered/spayed + '1.8': { min: 1.6, max: 1.8 }, // Adult intact + '1.0': { min: 1.0, max: 1.0 }, // Weight loss (fixed) + '1.7': { min: 1.2, max: 1.8 }, // Weight gain (wide range) + '5.0': { min: 5.0, max: 5.0 }, // Working heavy (upper bound) + '1.1': { min: 1.1, max: 1.1 } // Senior (no range) + }; + + const key = factor.toFixed(1); + return ranges[key] || { min: factor, max: factor }; + } + validateInput(value, min = 0, isInteger = false) { const num = parseFloat(value); if (isNaN(num) || num < min) return false; @@ -1184,10 +1205,21 @@ const rer = this.calculateRER(weightKg); const mer = this.calculateMER(rer, factor); - this.currentMER = mer; + // Calculate range for MER + const range = this.getLifeStageRange(factor); + this.currentMERMin = this.calculateMER(rer, range.min); + this.currentMERMax = this.calculateMER(rer, range.max); + this.currentMER = mer; // Keep middle/selected value for compatibility rerValue.textContent = this.formatNumber(rer, 0) + ' cal/day'; - merValue.textContent = this.formatNumber(mer, 0) + ' cal/day'; + + // Show MER as range if applicable + if (range.min !== range.max) { + merValue.textContent = this.formatNumber(this.currentMERMin, 0) + '-' + + this.formatNumber(this.currentMERMax, 0) + ' cal/day'; + } else { + merValue.textContent = this.formatNumber(mer, 0) + ' cal/day'; + } calorieResults.style.display = 'block'; this.updateFoodCalculations(); @@ -1222,6 +1254,9 @@ updateFoodCalculations() { if (this.currentMER === 0) return; + // Check if we have a range + const hasRange = this.currentMERMin !== this.currentMERMax; + const daysInput = document.getElementById('days'); const unitSelect = document.getElementById('unit'); const dailyFoodResults = document.getElementById('dailyFoodResults'); @@ -1294,43 +1329,70 @@ if (energyPer100g && energyPer100g > 0.1 && fs.percentage > 0) { const dailyCaloriesForThisFood = (this.currentMER * fs.percentage) / 100; + // Calculate range values if applicable + const dailyCaloriesMin = hasRange ? (this.currentMERMin * fs.percentage) / 100 : dailyCaloriesForThisFood; + const dailyCaloriesMax = hasRange ? (this.currentMERMax * fs.percentage) / 100 : dailyCaloriesForThisFood; + let dailyGramsForThisFood; + let dailyGramsMin, dailyGramsMax; let dailyCupsForThisFood = null; + let dailyCupsMin, dailyCupsMax; // For kcal/cup, calculate cups directly from calories if (fs.energyUnit === 'kcalcup' && fs.energy) { const caloriesPerCup = parseFloat(fs.energy); dailyCupsForThisFood = dailyCaloriesForThisFood / caloriesPerCup; + dailyCupsMin = dailyCaloriesMin / caloriesPerCup; + dailyCupsMax = dailyCaloriesMax / caloriesPerCup; // We still need grams for total calculation, use approximation dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 100; - console.log('Cups calculation:', { - caloriesPerCup, - dailyCaloriesForThisFood, - dailyCupsForThisFood, - dailyGramsForThisFood - }); + dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100; + dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100; } else { // For other units, calculate grams normally dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 100; + dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100; + dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100; } // Calculate per-meal amounts if needed const displayGrams = this.showPerMeal ? dailyGramsForThisFood / this.mealsPerDay : dailyGramsForThisFood; + const displayGramsMin = this.showPerMeal ? dailyGramsMin / this.mealsPerDay : dailyGramsMin; + const displayGramsMax = this.showPerMeal ? dailyGramsMax / this.mealsPerDay : dailyGramsMax; + const displayCups = dailyCupsForThisFood !== null ? (this.showPerMeal ? dailyCupsForThisFood / this.mealsPerDay : dailyCupsForThisFood) : null; + const displayCupsMin = dailyCupsMin !== undefined ? + (this.showPerMeal ? dailyCupsMin / this.mealsPerDay : dailyCupsMin) : null; + const displayCupsMax = dailyCupsMax !== undefined ? + (this.showPerMeal ? dailyCupsMax / this.mealsPerDay : dailyCupsMax) : null; + const displayCalories = this.showPerMeal ? dailyCaloriesForThisFood / this.mealsPerDay : dailyCaloriesForThisFood; + const displayCaloriesMin = this.showPerMeal ? dailyCaloriesMin / this.mealsPerDay : dailyCaloriesMin; + const displayCaloriesMax = this.showPerMeal ? dailyCaloriesMax / this.mealsPerDay : dailyCaloriesMax; foodBreakdowns.push({ name: fs.name, percentage: fs.percentage, dailyGrams: dailyGramsForThisFood, + dailyGramsMin: dailyGramsMin, + dailyGramsMax: dailyGramsMax, displayGrams: displayGrams, + displayGramsMin: displayGramsMin, + displayGramsMax: displayGramsMax, dailyCups: dailyCupsForThisFood, + dailyCupsMin: dailyCupsMin, + dailyCupsMax: dailyCupsMax, displayCups: displayCups, + displayCupsMin: displayCupsMin, + displayCupsMax: displayCupsMax, calories: dailyCaloriesForThisFood, displayCalories: displayCalories, + displayCaloriesMin: displayCaloriesMin, + displayCaloriesMax: displayCaloriesMax, isLocked: fs.isLocked, hasEnergyContent: true, + hasRange: hasRange, foodSource: fs // Store reference for cups conversion }); @@ -1435,12 +1497,23 @@ if (unit === 'cups') { // For cups, use the pre-calculated cups value if available if (breakdown.displayCups !== null) { - valueContent = `${this.formatNumber(breakdown.displayCups, decimals)} ${unitLabel}${frequencySuffix}`; + if (breakdown.hasRange && breakdown.displayCupsMin !== breakdown.displayCupsMax) { + valueContent = `${this.formatNumber(breakdown.displayCupsMin, decimals)}-${this.formatNumber(breakdown.displayCupsMax, decimals)} ${unitLabel}${frequencySuffix}`; + } else { + valueContent = `${this.formatNumber(breakdown.displayCups, decimals)} ${unitLabel}${frequencySuffix}`; + } } else { valueContent = `N/A`; } } else { - valueContent = `${this.formatNumber(this.convertUnits(breakdown.displayGrams, unit), decimals)} ${unitLabel}${frequencySuffix}`; + // For other units (g, kg, oz, lb) + if (breakdown.hasRange && breakdown.displayGramsMin !== breakdown.displayGramsMax) { + const minConverted = this.convertUnits(breakdown.displayGramsMin, unit); + const maxConverted = this.convertUnits(breakdown.displayGramsMax, unit); + valueContent = `${this.formatNumber(minConverted, decimals)}-${this.formatNumber(maxConverted, decimals)} ${unitLabel}${frequencySuffix}`; + } else { + valueContent = `${this.formatNumber(this.convertUnits(breakdown.displayGrams, unit), decimals)} ${unitLabel}${frequencySuffix}`; + } } } else { valueContent = `⚠️`; @@ -1478,23 +1551,53 @@ if (validForCups) { // Calculate total cups using pre-calculated values let totalCups = 0; + let totalCupsMin = 0; + let totalCupsMax = 0; foodBreakdowns.forEach(breakdown => { if (breakdown.percentage > 0 && breakdown.displayCups !== null) { totalCups += breakdown.displayCups; + if (breakdown.hasRange) { + totalCupsMin += breakdown.displayCupsMin || breakdown.displayCups; + totalCupsMax += breakdown.displayCupsMax || breakdown.displayCups; + } else { + totalCupsMin += breakdown.displayCups; + totalCupsMax += breakdown.displayCups; + } } }); - console.log('Total cups display:', { - totalCups, - displayTotal, - foodBreakdowns: foodBreakdowns.map(b => ({ name: b.name, displayCups: b.displayCups })) - }); - totalDisplayText = this.formatNumber(totalCups, decimals) + ` ${unitLabel}${frequencySuffix}`; + + if (hasRange && totalCupsMin !== totalCupsMax) { + totalDisplayText = `${this.formatNumber(totalCupsMin, decimals)}-${this.formatNumber(totalCupsMax, decimals)} ${unitLabel}${frequencySuffix}`; + } else { + totalDisplayText = this.formatNumber(totalCups, decimals) + ` ${unitLabel}${frequencySuffix}`; + } } else { totalDisplayText = 'Mixed units - see breakdown'; } } else { - convertedTotal = this.convertUnits(displayTotal, unit); - totalDisplayText = this.formatNumber(convertedTotal, decimals) + ` ${unitLabel}${frequencySuffix}`; + // Calculate totals for ranges + if (hasRange) { + let totalGramsMin = 0; + let totalGramsMax = 0; + foodBreakdowns.forEach(breakdown => { + if (breakdown.percentage > 0 && breakdown.hasEnergyContent) { + totalGramsMin += breakdown.displayGramsMin || breakdown.displayGrams; + totalGramsMax += breakdown.displayGramsMax || breakdown.displayGrams; + } + }); + + const convertedMin = this.convertUnits(totalGramsMin, unit); + const convertedMax = this.convertUnits(totalGramsMax, unit); + + if (totalGramsMin !== totalGramsMax) { + totalDisplayText = `${this.formatNumber(convertedMin, decimals)}-${this.formatNumber(convertedMax, decimals)} ${unitLabel}${frequencySuffix}`; + } else { + totalDisplayText = this.formatNumber(convertedMin, decimals) + ` ${unitLabel}${frequencySuffix}`; + } + } else { + convertedTotal = this.convertUnits(displayTotal, unit); + totalDisplayText = this.formatNumber(convertedTotal, decimals) + ` ${unitLabel}${frequencySuffix}`; + } } dailyFoodValue.textContent = totalDisplayText; diff --git a/sundog-dog-food-calculator.js b/sundog-dog-food-calculator.js index 8d8832c..3effe59 100644 --- a/sundog-dog-food-calculator.js +++ b/sundog-dog-food-calculator.js @@ -47,7 +47,7 @@ } .dog-calculator-container { - max-width: 600px; + max-width: 640px; margin: 0 auto; padding: 24px; box-sizing: border-box; @@ -228,6 +228,7 @@ justify-content: space-between; align-items: center; margin-bottom: 12px; + gap: 10px; /* Add gap between label and value */ } .dog-calculator-result-item:last-child { @@ -247,6 +248,7 @@ padding: 4px 12px; background: rgba(241, 154, 95, 0.15); border-radius: 4px; + white-space: nowrap; /* Prevent text from wrapping to multiple lines */ } .dog-calculator-collapsible { @@ -558,6 +560,24 @@ flex-direction: column; align-items: flex-start; } + + /* Stack result items vertically on small screens */ + .dog-calculator-result-item { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .dog-calculator-result-label { + margin-right: 0; + font-size: 0.9rem; + } + + .dog-calculator-result-value { + font-size: 1rem; + align-self: stretch; + text-align: center; + } } /* Dark theme - manual override */ @@ -2105,6 +2125,8 @@ const CALCULATOR_CONFIG = { this.theme = this.options.theme; this.scale = this.options.scale; this.currentMER = 0; + this.currentMERMin = 0; // For range calculations + this.currentMERMax = 0; // For range calculations this.isImperial = false; @@ -3372,6 +3394,25 @@ const CALCULATOR_CONFIG = { return rer * factor; } + // Get the range multipliers for each life stage + getLifeStageRange(factor) { + // Define ranges based on the reference image + const ranges = { + '3.0': { min: 3.0, max: 3.0 }, // Puppy 0-4 months (no range) + '2.0': { min: 2.0, max: 2.0 }, // Puppy 4m-adult OR Working light (no range for puppies) + '1.2': { min: 1.2, max: 1.4 }, // Adult inactive/obese + '1.6': { min: 1.4, max: 1.6 }, // Adult neutered/spayed + '1.8': { min: 1.6, max: 1.8 }, // Adult intact + '1.0': { min: 1.0, max: 1.0 }, // Weight loss (fixed) + '1.7': { min: 1.2, max: 1.8 }, // Weight gain (wide range) + '5.0': { min: 5.0, max: 5.0 }, // Working heavy (upper bound) + '1.1': { min: 1.1, max: 1.1 } // Senior (no range) + }; + + const key = factor.toFixed(1); + return ranges[key] || { min: factor, max: factor }; + } + validateInput(value, min = 0, isInteger = false) { const num = parseFloat(value); if (isNaN(num) || num < min) return false; @@ -3503,10 +3544,21 @@ const CALCULATOR_CONFIG = { const rer = this.calculateRER(weightKg); const mer = this.calculateMER(rer, factor); - this.currentMER = mer; + // Calculate range for MER + const range = this.getLifeStageRange(factor); + this.currentMERMin = this.calculateMER(rer, range.min); + this.currentMERMax = this.calculateMER(rer, range.max); + this.currentMER = mer; // Keep middle/selected value for compatibility rerValue.textContent = this.formatNumber(rer, 0) + ' cal/day'; - merValue.textContent = this.formatNumber(mer, 0) + ' cal/day'; + + // Show MER as range if applicable + if (range.min !== range.max) { + merValue.textContent = this.formatNumber(this.currentMERMin, 0) + '-' + + this.formatNumber(this.currentMERMax, 0) + ' cal/day'; + } else { + merValue.textContent = this.formatNumber(mer, 0) + ' cal/day'; + } calorieResults.style.display = 'block'; this.updateFoodCalculations(); @@ -3541,6 +3593,9 @@ const CALCULATOR_CONFIG = { updateFoodCalculations() { if (this.currentMER === 0) return; + // Check if we have a range + const hasRange = this.currentMERMin !== this.currentMERMax; + const daysInput = this.container.querySelector('#days'); const unitSelect = this.container.querySelector('#unit'); const dailyFoodResults = this.container.querySelector('#dailyFoodResults'); @@ -3613,43 +3668,70 @@ const CALCULATOR_CONFIG = { if (energyPer100g && energyPer100g > 0.1 && fs.percentage > 0) { const dailyCaloriesForThisFood = (this.currentMER * fs.percentage) / 100; + // Calculate range values if applicable + const dailyCaloriesMin = hasRange ? (this.currentMERMin * fs.percentage) / 100 : dailyCaloriesForThisFood; + const dailyCaloriesMax = hasRange ? (this.currentMERMax * fs.percentage) / 100 : dailyCaloriesForThisFood; + let dailyGramsForThisFood; + let dailyGramsMin, dailyGramsMax; let dailyCupsForThisFood = null; + let dailyCupsMin, dailyCupsMax; // For kcal/cup, calculate cups directly from calories if (fs.energyUnit === 'kcalcup' && fs.energy) { const caloriesPerCup = parseFloat(fs.energy); dailyCupsForThisFood = dailyCaloriesForThisFood / caloriesPerCup; + dailyCupsMin = dailyCaloriesMin / caloriesPerCup; + dailyCupsMax = dailyCaloriesMax / caloriesPerCup; // We still need grams for total calculation, use approximation dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 100; - console.log('Cups calculation:', { - caloriesPerCup, - dailyCaloriesForThisFood, - dailyCupsForThisFood, - dailyGramsForThisFood - }); + dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100; + dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100; } else { // For other units, calculate grams normally dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 100; + dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100; + dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100; } // Calculate per-meal amounts if needed const displayGrams = this.showPerMeal ? dailyGramsForThisFood / this.mealsPerDay : dailyGramsForThisFood; + const displayGramsMin = this.showPerMeal ? dailyGramsMin / this.mealsPerDay : dailyGramsMin; + const displayGramsMax = this.showPerMeal ? dailyGramsMax / this.mealsPerDay : dailyGramsMax; + const displayCups = dailyCupsForThisFood !== null ? (this.showPerMeal ? dailyCupsForThisFood / this.mealsPerDay : dailyCupsForThisFood) : null; + const displayCupsMin = dailyCupsMin !== undefined ? + (this.showPerMeal ? dailyCupsMin / this.mealsPerDay : dailyCupsMin) : null; + const displayCupsMax = dailyCupsMax !== undefined ? + (this.showPerMeal ? dailyCupsMax / this.mealsPerDay : dailyCupsMax) : null; + const displayCalories = this.showPerMeal ? dailyCaloriesForThisFood / this.mealsPerDay : dailyCaloriesForThisFood; + const displayCaloriesMin = this.showPerMeal ? dailyCaloriesMin / this.mealsPerDay : dailyCaloriesMin; + const displayCaloriesMax = this.showPerMeal ? dailyCaloriesMax / this.mealsPerDay : dailyCaloriesMax; foodBreakdowns.push({ name: fs.name, percentage: fs.percentage, dailyGrams: dailyGramsForThisFood, + dailyGramsMin: dailyGramsMin, + dailyGramsMax: dailyGramsMax, displayGrams: displayGrams, + displayGramsMin: displayGramsMin, + displayGramsMax: displayGramsMax, dailyCups: dailyCupsForThisFood, + dailyCupsMin: dailyCupsMin, + dailyCupsMax: dailyCupsMax, displayCups: displayCups, + displayCupsMin: displayCupsMin, + displayCupsMax: displayCupsMax, calories: dailyCaloriesForThisFood, displayCalories: displayCalories, + displayCaloriesMin: displayCaloriesMin, + displayCaloriesMax: displayCaloriesMax, isLocked: fs.isLocked, hasEnergyContent: true, + hasRange: hasRange, foodSource: fs // Store reference for cups conversion }); @@ -3754,12 +3836,23 @@ const CALCULATOR_CONFIG = { if (unit === 'cups') { // For cups, use the pre-calculated cups value if available if (breakdown.displayCups !== null) { - valueContent = `${this.formatNumber(breakdown.displayCups, decimals)} ${unitLabel}${frequencySuffix}`; + if (breakdown.hasRange && breakdown.displayCupsMin !== breakdown.displayCupsMax) { + valueContent = `${this.formatNumber(breakdown.displayCupsMin, decimals)}-${this.formatNumber(breakdown.displayCupsMax, decimals)} ${unitLabel}${frequencySuffix}`; + } else { + valueContent = `${this.formatNumber(breakdown.displayCups, decimals)} ${unitLabel}${frequencySuffix}`; + } } else { valueContent = `N/A`; } } else { - valueContent = `${this.formatNumber(this.convertUnits(breakdown.displayGrams, unit), decimals)} ${unitLabel}${frequencySuffix}`; + // For other units (g, kg, oz, lb) + if (breakdown.hasRange && breakdown.displayGramsMin !== breakdown.displayGramsMax) { + const minConverted = this.convertUnits(breakdown.displayGramsMin, unit); + const maxConverted = this.convertUnits(breakdown.displayGramsMax, unit); + valueContent = `${this.formatNumber(minConverted, decimals)}-${this.formatNumber(maxConverted, decimals)} ${unitLabel}${frequencySuffix}`; + } else { + valueContent = `${this.formatNumber(this.convertUnits(breakdown.displayGrams, unit), decimals)} ${unitLabel}${frequencySuffix}`; + } } } else { valueContent = `⚠️`; @@ -3797,23 +3890,53 @@ const CALCULATOR_CONFIG = { if (validForCups) { // Calculate total cups using pre-calculated values let totalCups = 0; + let totalCupsMin = 0; + let totalCupsMax = 0; foodBreakdowns.forEach(breakdown => { if (breakdown.percentage > 0 && breakdown.displayCups !== null) { totalCups += breakdown.displayCups; + if (breakdown.hasRange) { + totalCupsMin += breakdown.displayCupsMin || breakdown.displayCups; + totalCupsMax += breakdown.displayCupsMax || breakdown.displayCups; + } else { + totalCupsMin += breakdown.displayCups; + totalCupsMax += breakdown.displayCups; + } } }); - console.log('Total cups display:', { - totalCups, - displayTotal, - foodBreakdowns: foodBreakdowns.map(b => ({ name: b.name, displayCups: b.displayCups })) - }); - totalDisplayText = this.formatNumber(totalCups, decimals) + ` ${unitLabel}${frequencySuffix}`; + + if (hasRange && totalCupsMin !== totalCupsMax) { + totalDisplayText = `${this.formatNumber(totalCupsMin, decimals)}-${this.formatNumber(totalCupsMax, decimals)} ${unitLabel}${frequencySuffix}`; + } else { + totalDisplayText = this.formatNumber(totalCups, decimals) + ` ${unitLabel}${frequencySuffix}`; + } } else { totalDisplayText = 'Mixed units - see breakdown'; } } else { - convertedTotal = this.convertUnits(displayTotal, unit); - totalDisplayText = this.formatNumber(convertedTotal, decimals) + ` ${unitLabel}${frequencySuffix}`; + // Calculate totals for ranges + if (hasRange) { + let totalGramsMin = 0; + let totalGramsMax = 0; + foodBreakdowns.forEach(breakdown => { + if (breakdown.percentage > 0 && breakdown.hasEnergyContent) { + totalGramsMin += breakdown.displayGramsMin || breakdown.displayGrams; + totalGramsMax += breakdown.displayGramsMax || breakdown.displayGrams; + } + }); + + const convertedMin = this.convertUnits(totalGramsMin, unit); + const convertedMax = this.convertUnits(totalGramsMax, unit); + + if (totalGramsMin !== totalGramsMax) { + totalDisplayText = `${this.formatNumber(convertedMin, decimals)}-${this.formatNumber(convertedMax, decimals)} ${unitLabel}${frequencySuffix}`; + } else { + totalDisplayText = this.formatNumber(convertedMin, decimals) + ` ${unitLabel}${frequencySuffix}`; + } + } else { + convertedTotal = this.convertUnits(displayTotal, unit); + totalDisplayText = this.formatNumber(convertedTotal, decimals) + ` ${unitLabel}${frequencySuffix}`; + } } dailyFoodValue.textContent = totalDisplayText;