Updates
This commit is contained in:
parent
da9fd20ffb
commit
73c4648978
233
iframe.html
233
iframe.html
@ -2171,11 +2171,12 @@ const CALCULATOR_CONFIG = {
|
|||||||
// Seed three sources for Kaya's transition
|
// Seed three sources for Kaya's transition
|
||||||
const gc = {
|
const gc = {
|
||||||
id: this.generateFoodSourceId(),
|
id: this.generateFoodSourceId(),
|
||||||
name: 'Fred & Felia, gently cooked',
|
name: 'Fred & Felia (Junior Huhn)',
|
||||||
energy: '115',
|
energy: '115',
|
||||||
energyUnit: 'kcal100g',
|
energyUnit: 'kcal100g',
|
||||||
percentage: 5,
|
percentage: 5,
|
||||||
isLocked: false
|
isLocked: false,
|
||||||
|
chartType: 'gc'
|
||||||
};
|
};
|
||||||
this.foodSources.push(gc);
|
this.foodSources.push(gc);
|
||||||
this.renderFoodSource(gc);
|
this.renderFoodSource(gc);
|
||||||
@ -2183,11 +2184,12 @@ const CALCULATOR_CONFIG = {
|
|||||||
|
|
||||||
const kibble = {
|
const kibble = {
|
||||||
id: this.generateFoodSourceId(),
|
id: this.generateFoodSourceId(),
|
||||||
name: 'Eukanuba, kibble',
|
name: 'Eukanuba (Large Breed Fresh Chicken)',
|
||||||
energy: '372',
|
energy: '372',
|
||||||
energyUnit: 'kcal100g',
|
energyUnit: 'kcal100g',
|
||||||
percentage: 95,
|
percentage: 95,
|
||||||
isLocked: false
|
isLocked: false,
|
||||||
|
chartType: 'kibble'
|
||||||
};
|
};
|
||||||
this.foodSources.push(kibble);
|
this.foodSources.push(kibble);
|
||||||
this.renderFoodSource(kibble);
|
this.renderFoodSource(kibble);
|
||||||
@ -2199,7 +2201,8 @@ const CALCULATOR_CONFIG = {
|
|||||||
energy: '',
|
energy: '',
|
||||||
energyUnit: 'kcal100g',
|
energyUnit: 'kcal100g',
|
||||||
percentage: 0,
|
percentage: 0,
|
||||||
isLocked: false
|
isLocked: false,
|
||||||
|
chartType: null
|
||||||
};
|
};
|
||||||
this.foodSources.push(treats);
|
this.foodSources.push(treats);
|
||||||
this.renderFoodSource(treats);
|
this.renderFoodSource(treats);
|
||||||
@ -3239,58 +3242,29 @@ const CALCULATOR_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCalorieCalculations() {
|
updateCalorieCalculations() {
|
||||||
// Kaya-specific: compute daily kcal target from age + kibble energy
|
// Kaya-specific: only track age and trigger recompute
|
||||||
const ageInput = document.getElementById('ageMonths');
|
const ageInput = document.getElementById('ageMonths');
|
||||||
const ageClampNote = document.getElementById('ageClampNote');
|
const ageClampNote = document.getElementById('ageClampNote');
|
||||||
|
|
||||||
// Default: hide clamp note
|
|
||||||
if (ageClampNote) ageClampNote.classList.add('dog-calculator-hidden');
|
if (ageClampNote) ageClampNote.classList.add('dog-calculator-hidden');
|
||||||
|
|
||||||
if (!ageInput || !ageInput.value) {
|
if (!ageInput || ageInput.value === '') {
|
||||||
this.currentMER = 0;
|
this.currentAge = null;
|
||||||
this.currentMERMin = 0;
|
this.updateFoodCalculations();
|
||||||
this.currentMERMax = 0;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let age = parseFloat(ageInput.value);
|
let age = parseFloat(ageInput.value);
|
||||||
if (isNaN(age)) {
|
if (isNaN(age)) {
|
||||||
this.currentMER = 0;
|
this.currentAge = null;
|
||||||
this.currentMERMin = 0;
|
this.updateFoodCalculations();
|
||||||
this.currentMERMax = 0;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp age to [2, 12]
|
|
||||||
if (age < 2) { age = 2; if (ageClampNote) ageClampNote.classList.remove('dog-calculator-hidden'); }
|
if (age < 2) { age = 2; if (ageClampNote) ageClampNote.classList.remove('dog-calculator-hidden'); }
|
||||||
if (age > 12) { age = 12; if (ageClampNote) ageClampNote.classList.remove('dog-calculator-hidden'); }
|
if (age > 12) { age = 12; if (ageClampNote) ageClampNote.classList.remove('dog-calculator-hidden'); }
|
||||||
|
|
||||||
// Bucket pills removed
|
this.currentAge = age;
|
||||||
|
|
||||||
// Calculate interpolated kibble grams/day for 30 kg at this age
|
|
||||||
const kibbleGrams = this.getKayaKibbleGramsForAge(age);
|
|
||||||
|
|
||||||
// Get kibble reference energy density in kcal/100g
|
|
||||||
let kibbleEnergyPer100g = null;
|
|
||||||
if (this.kibbleRefId) {
|
|
||||||
const kibbleRef = this.foodSources.find(fs => fs.id === this.kibbleRefId);
|
|
||||||
kibbleEnergyPer100g = kibbleRef ? this.getFoodSourceEnergyPer100g(kibbleRef) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!kibbleEnergyPer100g || kibbleEnergyPer100g <= 0) {
|
|
||||||
// Cannot compute without kibble energy density
|
|
||||||
this.currentMER = 0;
|
|
||||||
this.currentMERMin = 0;
|
|
||||||
this.currentMERMax = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const kcalPerGram = kibbleEnergyPer100g / 100.0;
|
|
||||||
const dailyKcal = kibbleGrams * kcalPerGram;
|
|
||||||
this.currentMER = dailyKcal;
|
|
||||||
this.currentMERMin = dailyKcal;
|
|
||||||
this.currentMERMax = dailyKcal;
|
|
||||||
|
|
||||||
this.updateFoodCalculations();
|
this.updateFoodCalculations();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3321,38 +3295,38 @@ const CALCULATOR_CONFIG = {
|
|||||||
return lowerVal + (upperVal - lowerVal) * t;
|
return lowerVal + (upperVal - lowerVal) * t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kaya: GC chart interpolation across buckets
|
||||||
|
getGCChartGramsForAge(ageMonths) {
|
||||||
|
// Buckets per guideline with boundary rule (5.0 → later bucket, 7.0 → later bucket)
|
||||||
|
// <5 mo (2.0–<5.0): 950→1350
|
||||||
|
// 5–6 mo (5.0–6.0): 1250→1550; (6.0–<7.0): hold 1550
|
||||||
|
// 7–12 mo (7.0–12.0): 1300→1500
|
||||||
|
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
||||||
|
const age = clamp(ageMonths, 2, 12);
|
||||||
|
if (age < 5) {
|
||||||
|
const t = (age - 2) / (5 - 2);
|
||||||
|
return 950 + t * (1350 - 950);
|
||||||
|
} else if (age < 6) {
|
||||||
|
const t = (age - 5) / (6 - 5);
|
||||||
|
return 1250 + t * (1550 - 1250);
|
||||||
|
} else if (age < 7) {
|
||||||
|
return 1550; // hold upper value until 7.0
|
||||||
|
} else {
|
||||||
|
const t = (age - 7) / (12 - 7);
|
||||||
|
return 1300 + t * (1500 - 1300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
updateCupsButtonState() {
|
updateCupsButtonState() {
|
||||||
const cupsButton = document.getElementById('cupsButton');
|
// Cups UI is not used in this configuration
|
||||||
if (!cupsButton) return;
|
return;
|
||||||
|
|
||||||
// Check if any food source has kcal/cup selected
|
|
||||||
const hasKcalCup = this.foodSources.some(fs =>
|
|
||||||
fs.energyUnit === 'kcalcup' && fs.energy && parseFloat(fs.energy) > 0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasKcalCup) {
|
|
||||||
cupsButton.disabled = false;
|
|
||||||
cupsButton.title = 'Show amounts in cups';
|
|
||||||
} else {
|
|
||||||
cupsButton.disabled = true;
|
|
||||||
cupsButton.title = 'Available when using kcal/cup measurement';
|
|
||||||
|
|
||||||
// If cups was selected, switch back to grams
|
|
||||||
const unitSelect = document.getElementById('unit');
|
|
||||||
if (unitSelect && unitSelect.value === 'cups') {
|
|
||||||
unitSelect.value = 'g';
|
|
||||||
this.setActiveUnitButton('g');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFoodCalculations() {
|
updateFoodCalculations() {
|
||||||
if (this.currentMER === 0) return;
|
// Chart-first: no MER check
|
||||||
|
const hasRange = false;
|
||||||
// Check if we have a range
|
|
||||||
const hasRange = this.currentMERMin !== this.currentMERMax;
|
|
||||||
|
|
||||||
const daysInput = document.getElementById('days');
|
const daysInput = document.getElementById('days');
|
||||||
const unitSelect = document.getElementById('unit');
|
const unitSelect = document.getElementById('unit');
|
||||||
@ -3416,101 +3390,80 @@ const CALCULATOR_CONFIG = {
|
|||||||
|
|
||||||
const numDays = parseInt(days);
|
const numDays = parseInt(days);
|
||||||
|
|
||||||
// Calculate per-food breakdown
|
// Require a valid age for chart-first outputs
|
||||||
|
const ageInput = document.getElementById('ageMonths');
|
||||||
|
let age = ageInput && ageInput.value !== '' ? parseFloat(ageInput.value) : null;
|
||||||
|
if (age !== null) {
|
||||||
|
if (isNaN(age)) age = null;
|
||||||
|
if (age !== null) {
|
||||||
|
if (age < 2) age = 2;
|
||||||
|
if (age > 12) age = 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate per-food breakdown (chart-first)
|
||||||
const foodBreakdowns = [];
|
const foodBreakdowns = [];
|
||||||
let totalDailyGrams = 0;
|
let totalDailyGrams = 0;
|
||||||
let hasValidFoods = false;
|
let hasValidFoods = false;
|
||||||
|
|
||||||
|
// First pass: charted baseline
|
||||||
|
let chartedKcal = 0;
|
||||||
|
let chartedPercent = 0;
|
||||||
|
const firstPass = [];
|
||||||
this.foodSources.forEach(fs => {
|
this.foodSources.forEach(fs => {
|
||||||
const energyPer100g = this.getFoodSourceEnergyPer100g(fs);
|
const energyPer100g = this.getFoodSourceEnergyPer100g(fs);
|
||||||
|
let chartGrams = null;
|
||||||
|
if (fs.chartType === 'gc') {
|
||||||
|
chartGrams = age !== null ? this.getGCChartGramsForAge(age) : null;
|
||||||
|
} else if (fs.chartType === 'kibble') {
|
||||||
|
chartGrams = age !== null ? this.getKayaKibbleGramsForAge(age) : null;
|
||||||
|
}
|
||||||
|
const gramsPortion = (chartGrams !== null && chartGrams !== undefined) ? (chartGrams * (fs.percentage || 0) / 100) : null;
|
||||||
|
if (gramsPortion !== null && energyPer100g && energyPer100g > 0) {
|
||||||
|
chartedKcal += gramsPortion * (energyPer100g / 100);
|
||||||
|
chartedPercent += (fs.percentage || 0);
|
||||||
|
}
|
||||||
|
firstPass.push({ fs, energyPer100g, gramsPortion });
|
||||||
|
});
|
||||||
|
|
||||||
if (energyPer100g && energyPer100g > 0.1 && fs.percentage > 0) {
|
const kcalPerPercent = chartedPercent > 0 ? (chartedKcal / chartedPercent) : null;
|
||||||
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;
|
// Second pass: finalize amounts
|
||||||
let dailyGramsMin, dailyGramsMax;
|
firstPass.forEach(({ fs, energyPer100g, gramsPortion }) => {
|
||||||
let dailyCupsForThisFood = null;
|
let dailyGramsForThisFood = 0;
|
||||||
let dailyCupsMin, dailyCupsMax;
|
let hasEnergyContent = !!(energyPer100g && energyPer100g > 0);
|
||||||
|
if ((fs.chartType === 'gc' || fs.chartType === 'kibble')) {
|
||||||
// For kcal/cup, calculate cups directly from calories
|
if (gramsPortion !== null) {
|
||||||
if (fs.energyUnit === 'kcalcup' && fs.energy) {
|
dailyGramsForThisFood = gramsPortion;
|
||||||
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;
|
|
||||||
dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100;
|
|
||||||
dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100;
|
|
||||||
} else {
|
} else {
|
||||||
// For other units, calculate grams normally
|
if (hasEnergyContent && kcalPerPercent && (fs.percentage || 0) > 0) {
|
||||||
dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 100;
|
const perGramKcal = energyPer100g / 100;
|
||||||
dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100;
|
const kcalForFood = (fs.percentage || 0) * kcalPerPercent;
|
||||||
dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100;
|
dailyGramsForThisFood = kcalForFood / perGramKcal;
|
||||||
|
} else {
|
||||||
|
hasEnergyContent = false;
|
||||||
|
dailyGramsForThisFood = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate per-meal amounts if needed
|
|
||||||
const displayGrams = this.showPerMeal ? dailyGramsForThisFood / this.mealsPerDay : dailyGramsForThisFood;
|
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({
|
foodBreakdowns.push({
|
||||||
name: fs.name,
|
name: fs.name,
|
||||||
percentage: fs.percentage,
|
percentage: fs.percentage,
|
||||||
dailyGrams: dailyGramsForThisFood,
|
dailyGrams: dailyGramsForThisFood,
|
||||||
dailyGramsMin: dailyGramsMin,
|
|
||||||
dailyGramsMax: dailyGramsMax,
|
|
||||||
displayGrams: displayGrams,
|
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
|
|
||||||
});
|
|
||||||
|
|
||||||
totalDailyGrams += dailyGramsForThisFood;
|
|
||||||
hasValidFoods = true;
|
|
||||||
} else if (fs.percentage > 0) {
|
|
||||||
// Include food sources without energy content but show them as needing energy content
|
|
||||||
foodBreakdowns.push({
|
|
||||||
name: fs.name,
|
|
||||||
percentage: fs.percentage,
|
|
||||||
dailyGrams: 0,
|
|
||||||
displayGrams: 0,
|
|
||||||
dailyCups: null,
|
dailyCups: null,
|
||||||
displayCups: null,
|
displayCups: null,
|
||||||
calories: 0,
|
calories: hasEnergyContent ? (dailyGramsForThisFood * (energyPer100g / 100)) : 0,
|
||||||
displayCalories: 0,
|
displayCalories: hasEnergyContent ? (this.showPerMeal ? (dailyGramsForThisFood * (energyPer100g / 100)) / this.mealsPerDay : (dailyGramsForThisFood * (energyPer100g / 100))) : 0,
|
||||||
isLocked: fs.isLocked,
|
isLocked: fs.isLocked,
|
||||||
hasEnergyContent: false,
|
hasEnergyContent: hasEnergyContent,
|
||||||
foodSource: fs // Store reference for cups conversion
|
foodSource: fs
|
||||||
});
|
});
|
||||||
}
|
totalDailyGrams += dailyGramsForThisFood;
|
||||||
|
if (dailyGramsForThisFood > 0) hasValidFoods = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!hasValidFoods) {
|
if (!hasValidFoods) {
|
||||||
|
|||||||
@ -75,11 +75,12 @@
|
|||||||
// Seed three sources for Kaya's transition
|
// Seed three sources for Kaya's transition
|
||||||
const gc = {
|
const gc = {
|
||||||
id: this.generateFoodSourceId(),
|
id: this.generateFoodSourceId(),
|
||||||
name: 'Fred & Felia, gently cooked',
|
name: 'Fred & Felia (Junior Huhn)',
|
||||||
energy: '115',
|
energy: '115',
|
||||||
energyUnit: 'kcal100g',
|
energyUnit: 'kcal100g',
|
||||||
percentage: 5,
|
percentage: 5,
|
||||||
isLocked: false
|
isLocked: false,
|
||||||
|
chartType: 'gc'
|
||||||
};
|
};
|
||||||
this.foodSources.push(gc);
|
this.foodSources.push(gc);
|
||||||
this.renderFoodSource(gc);
|
this.renderFoodSource(gc);
|
||||||
@ -87,11 +88,12 @@
|
|||||||
|
|
||||||
const kibble = {
|
const kibble = {
|
||||||
id: this.generateFoodSourceId(),
|
id: this.generateFoodSourceId(),
|
||||||
name: 'Eukanuba, kibble',
|
name: 'Eukanuba (Large Breed Fresh Chicken)',
|
||||||
energy: '372',
|
energy: '372',
|
||||||
energyUnit: 'kcal100g',
|
energyUnit: 'kcal100g',
|
||||||
percentage: 95,
|
percentage: 95,
|
||||||
isLocked: false
|
isLocked: false,
|
||||||
|
chartType: 'kibble'
|
||||||
};
|
};
|
||||||
this.foodSources.push(kibble);
|
this.foodSources.push(kibble);
|
||||||
this.renderFoodSource(kibble);
|
this.renderFoodSource(kibble);
|
||||||
@ -103,7 +105,8 @@
|
|||||||
energy: '',
|
energy: '',
|
||||||
energyUnit: 'kcal100g',
|
energyUnit: 'kcal100g',
|
||||||
percentage: 0,
|
percentage: 0,
|
||||||
isLocked: false
|
isLocked: false,
|
||||||
|
chartType: null
|
||||||
};
|
};
|
||||||
this.foodSources.push(treats);
|
this.foodSources.push(treats);
|
||||||
this.renderFoodSource(treats);
|
this.renderFoodSource(treats);
|
||||||
@ -1143,58 +1146,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCalorieCalculations() {
|
updateCalorieCalculations() {
|
||||||
// Kaya-specific: compute daily kcal target from age + kibble energy
|
// Kaya-specific: only track age and trigger recompute
|
||||||
const ageInput = document.getElementById('ageMonths');
|
const ageInput = document.getElementById('ageMonths');
|
||||||
const ageClampNote = document.getElementById('ageClampNote');
|
const ageClampNote = document.getElementById('ageClampNote');
|
||||||
|
|
||||||
// Default: hide clamp note
|
|
||||||
if (ageClampNote) ageClampNote.classList.add('dog-calculator-hidden');
|
if (ageClampNote) ageClampNote.classList.add('dog-calculator-hidden');
|
||||||
|
|
||||||
if (!ageInput || !ageInput.value) {
|
if (!ageInput || ageInput.value === '') {
|
||||||
this.currentMER = 0;
|
this.currentAge = null;
|
||||||
this.currentMERMin = 0;
|
this.updateFoodCalculations();
|
||||||
this.currentMERMax = 0;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let age = parseFloat(ageInput.value);
|
let age = parseFloat(ageInput.value);
|
||||||
if (isNaN(age)) {
|
if (isNaN(age)) {
|
||||||
this.currentMER = 0;
|
this.currentAge = null;
|
||||||
this.currentMERMin = 0;
|
this.updateFoodCalculations();
|
||||||
this.currentMERMax = 0;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp age to [2, 12]
|
|
||||||
if (age < 2) { age = 2; if (ageClampNote) ageClampNote.classList.remove('dog-calculator-hidden'); }
|
if (age < 2) { age = 2; if (ageClampNote) ageClampNote.classList.remove('dog-calculator-hidden'); }
|
||||||
if (age > 12) { age = 12; if (ageClampNote) ageClampNote.classList.remove('dog-calculator-hidden'); }
|
if (age > 12) { age = 12; if (ageClampNote) ageClampNote.classList.remove('dog-calculator-hidden'); }
|
||||||
|
|
||||||
// Bucket pills removed
|
this.currentAge = age;
|
||||||
|
|
||||||
// Calculate interpolated kibble grams/day for 30 kg at this age
|
|
||||||
const kibbleGrams = this.getKayaKibbleGramsForAge(age);
|
|
||||||
|
|
||||||
// Get kibble reference energy density in kcal/100g
|
|
||||||
let kibbleEnergyPer100g = null;
|
|
||||||
if (this.kibbleRefId) {
|
|
||||||
const kibbleRef = this.foodSources.find(fs => fs.id === this.kibbleRefId);
|
|
||||||
kibbleEnergyPer100g = kibbleRef ? this.getFoodSourceEnergyPer100g(kibbleRef) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!kibbleEnergyPer100g || kibbleEnergyPer100g <= 0) {
|
|
||||||
// Cannot compute without kibble energy density
|
|
||||||
this.currentMER = 0;
|
|
||||||
this.currentMERMin = 0;
|
|
||||||
this.currentMERMax = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const kcalPerGram = kibbleEnergyPer100g / 100.0;
|
|
||||||
const dailyKcal = kibbleGrams * kcalPerGram;
|
|
||||||
this.currentMER = dailyKcal;
|
|
||||||
this.currentMERMin = dailyKcal;
|
|
||||||
this.currentMERMax = dailyKcal;
|
|
||||||
|
|
||||||
this.updateFoodCalculations();
|
this.updateFoodCalculations();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1225,38 +1199,38 @@
|
|||||||
return lowerVal + (upperVal - lowerVal) * t;
|
return lowerVal + (upperVal - lowerVal) * t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kaya: GC chart interpolation across buckets
|
||||||
|
getGCChartGramsForAge(ageMonths) {
|
||||||
|
// Buckets per guideline with boundary rule (5.0 → later bucket, 7.0 → later bucket)
|
||||||
|
// <5 mo (2.0–<5.0): 950→1350
|
||||||
|
// 5–6 mo (5.0–6.0): 1250→1550; (6.0–<7.0): hold 1550
|
||||||
|
// 7–12 mo (7.0–12.0): 1300→1500
|
||||||
|
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
||||||
|
const age = clamp(ageMonths, 2, 12);
|
||||||
|
if (age < 5) {
|
||||||
|
const t = (age - 2) / (5 - 2);
|
||||||
|
return 950 + t * (1350 - 950);
|
||||||
|
} else if (age < 6) {
|
||||||
|
const t = (age - 5) / (6 - 5);
|
||||||
|
return 1250 + t * (1550 - 1250);
|
||||||
|
} else if (age < 7) {
|
||||||
|
return 1550; // hold upper value until 7.0
|
||||||
|
} else {
|
||||||
|
const t = (age - 7) / (12 - 7);
|
||||||
|
return 1300 + t * (1500 - 1300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
updateCupsButtonState() {
|
updateCupsButtonState() {
|
||||||
const cupsButton = document.getElementById('cupsButton');
|
// Cups UI is not used in this configuration
|
||||||
if (!cupsButton) return;
|
return;
|
||||||
|
|
||||||
// Check if any food source has kcal/cup selected
|
|
||||||
const hasKcalCup = this.foodSources.some(fs =>
|
|
||||||
fs.energyUnit === 'kcalcup' && fs.energy && parseFloat(fs.energy) > 0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasKcalCup) {
|
|
||||||
cupsButton.disabled = false;
|
|
||||||
cupsButton.title = 'Show amounts in cups';
|
|
||||||
} else {
|
|
||||||
cupsButton.disabled = true;
|
|
||||||
cupsButton.title = 'Available when using kcal/cup measurement';
|
|
||||||
|
|
||||||
// If cups was selected, switch back to grams
|
|
||||||
const unitSelect = document.getElementById('unit');
|
|
||||||
if (unitSelect && unitSelect.value === 'cups') {
|
|
||||||
unitSelect.value = 'g';
|
|
||||||
this.setActiveUnitButton('g');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFoodCalculations() {
|
updateFoodCalculations() {
|
||||||
if (this.currentMER === 0) return;
|
// Chart-first: no MER check
|
||||||
|
const hasRange = false;
|
||||||
// Check if we have a range
|
|
||||||
const hasRange = this.currentMERMin !== this.currentMERMax;
|
|
||||||
|
|
||||||
const daysInput = document.getElementById('days');
|
const daysInput = document.getElementById('days');
|
||||||
const unitSelect = document.getElementById('unit');
|
const unitSelect = document.getElementById('unit');
|
||||||
@ -1320,101 +1294,80 @@
|
|||||||
|
|
||||||
const numDays = parseInt(days);
|
const numDays = parseInt(days);
|
||||||
|
|
||||||
// Calculate per-food breakdown
|
// Require a valid age for chart-first outputs
|
||||||
|
const ageInput = document.getElementById('ageMonths');
|
||||||
|
let age = ageInput && ageInput.value !== '' ? parseFloat(ageInput.value) : null;
|
||||||
|
if (age !== null) {
|
||||||
|
if (isNaN(age)) age = null;
|
||||||
|
if (age !== null) {
|
||||||
|
if (age < 2) age = 2;
|
||||||
|
if (age > 12) age = 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate per-food breakdown (chart-first)
|
||||||
const foodBreakdowns = [];
|
const foodBreakdowns = [];
|
||||||
let totalDailyGrams = 0;
|
let totalDailyGrams = 0;
|
||||||
let hasValidFoods = false;
|
let hasValidFoods = false;
|
||||||
|
|
||||||
|
// First pass: charted baseline
|
||||||
|
let chartedKcal = 0;
|
||||||
|
let chartedPercent = 0;
|
||||||
|
const firstPass = [];
|
||||||
this.foodSources.forEach(fs => {
|
this.foodSources.forEach(fs => {
|
||||||
const energyPer100g = this.getFoodSourceEnergyPer100g(fs);
|
const energyPer100g = this.getFoodSourceEnergyPer100g(fs);
|
||||||
|
let chartGrams = null;
|
||||||
|
if (fs.chartType === 'gc') {
|
||||||
|
chartGrams = age !== null ? this.getGCChartGramsForAge(age) : null;
|
||||||
|
} else if (fs.chartType === 'kibble') {
|
||||||
|
chartGrams = age !== null ? this.getKayaKibbleGramsForAge(age) : null;
|
||||||
|
}
|
||||||
|
const gramsPortion = (chartGrams !== null && chartGrams !== undefined) ? (chartGrams * (fs.percentage || 0) / 100) : null;
|
||||||
|
if (gramsPortion !== null && energyPer100g && energyPer100g > 0) {
|
||||||
|
chartedKcal += gramsPortion * (energyPer100g / 100);
|
||||||
|
chartedPercent += (fs.percentage || 0);
|
||||||
|
}
|
||||||
|
firstPass.push({ fs, energyPer100g, gramsPortion });
|
||||||
|
});
|
||||||
|
|
||||||
if (energyPer100g && energyPer100g > 0.1 && fs.percentage > 0) {
|
const kcalPerPercent = chartedPercent > 0 ? (chartedKcal / chartedPercent) : null;
|
||||||
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;
|
// Second pass: finalize amounts
|
||||||
let dailyGramsMin, dailyGramsMax;
|
firstPass.forEach(({ fs, energyPer100g, gramsPortion }) => {
|
||||||
let dailyCupsForThisFood = null;
|
let dailyGramsForThisFood = 0;
|
||||||
let dailyCupsMin, dailyCupsMax;
|
let hasEnergyContent = !!(energyPer100g && energyPer100g > 0);
|
||||||
|
if ((fs.chartType === 'gc' || fs.chartType === 'kibble')) {
|
||||||
// For kcal/cup, calculate cups directly from calories
|
if (gramsPortion !== null) {
|
||||||
if (fs.energyUnit === 'kcalcup' && fs.energy) {
|
dailyGramsForThisFood = gramsPortion;
|
||||||
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;
|
|
||||||
dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100;
|
|
||||||
dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100;
|
|
||||||
} else {
|
} else {
|
||||||
// For other units, calculate grams normally
|
if (hasEnergyContent && kcalPerPercent && (fs.percentage || 0) > 0) {
|
||||||
dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 100;
|
const perGramKcal = energyPer100g / 100;
|
||||||
dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100;
|
const kcalForFood = (fs.percentage || 0) * kcalPerPercent;
|
||||||
dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100;
|
dailyGramsForThisFood = kcalForFood / perGramKcal;
|
||||||
|
} else {
|
||||||
|
hasEnergyContent = false;
|
||||||
|
dailyGramsForThisFood = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate per-meal amounts if needed
|
|
||||||
const displayGrams = this.showPerMeal ? dailyGramsForThisFood / this.mealsPerDay : dailyGramsForThisFood;
|
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({
|
foodBreakdowns.push({
|
||||||
name: fs.name,
|
name: fs.name,
|
||||||
percentage: fs.percentage,
|
percentage: fs.percentage,
|
||||||
dailyGrams: dailyGramsForThisFood,
|
dailyGrams: dailyGramsForThisFood,
|
||||||
dailyGramsMin: dailyGramsMin,
|
|
||||||
dailyGramsMax: dailyGramsMax,
|
|
||||||
displayGrams: displayGrams,
|
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
|
|
||||||
});
|
|
||||||
|
|
||||||
totalDailyGrams += dailyGramsForThisFood;
|
|
||||||
hasValidFoods = true;
|
|
||||||
} else if (fs.percentage > 0) {
|
|
||||||
// Include food sources without energy content but show them as needing energy content
|
|
||||||
foodBreakdowns.push({
|
|
||||||
name: fs.name,
|
|
||||||
percentage: fs.percentage,
|
|
||||||
dailyGrams: 0,
|
|
||||||
displayGrams: 0,
|
|
||||||
dailyCups: null,
|
dailyCups: null,
|
||||||
displayCups: null,
|
displayCups: null,
|
||||||
calories: 0,
|
calories: hasEnergyContent ? (dailyGramsForThisFood * (energyPer100g / 100)) : 0,
|
||||||
displayCalories: 0,
|
displayCalories: hasEnergyContent ? (this.showPerMeal ? (dailyGramsForThisFood * (energyPer100g / 100)) / this.mealsPerDay : (dailyGramsForThisFood * (energyPer100g / 100))) : 0,
|
||||||
isLocked: fs.isLocked,
|
isLocked: fs.isLocked,
|
||||||
hasEnergyContent: false,
|
hasEnergyContent: hasEnergyContent,
|
||||||
foodSource: fs // Store reference for cups conversion
|
foodSource: fs
|
||||||
});
|
});
|
||||||
}
|
totalDailyGrams += dailyGramsForThisFood;
|
||||||
|
if (dailyGramsForThisFood > 0) hasValidFoods = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!hasValidFoods) {
|
if (!hasValidFoods) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user