mitai-jinkendo/frontend/src/utils/calc.js
Lars Stommer 89b6c0b072
Some checks are pending
Deploy to Raspberry Pi / deploy (push) Waiting to run
Build Test / build-frontend (push) Waiting to run
Build Test / lint-backend (push) Waiting to run
feat: initial commit – Mitai Jinkendo v9a
2026-03-16 13:35:11 +01:00

280 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export function calcBodyFat(method, skinfolds, sex, age) {
const vals = Object.values(skinfolds).filter(v => v > 0)
if (!vals.length) return null
const siri = (D) => Math.max(3, Math.min(60, (495 / D) - 450))
if (method === 'jackson3') {
const { chest=0, abdomen=0, suprailiac=0, triceps=0, thigh=0 } = skinfolds
const s = sex === 'm' ? chest+abdomen+thigh : triceps+suprailiac+thigh
if (s === 0) return null
const D = sex === 'm'
? 1.10938 - 0.0008267*s + 0.0000016*s*s - 0.0002574*age
: 1.0994921 - 0.0009929*s + 0.0000023*s*s - 0.0001392*age
return siri(D)
}
if (method === 'jackson7') {
const { chest=0, axilla=0, triceps=0, subscap=0, suprailiac=0, abdomen=0, thigh=0 } = skinfolds
const s = chest+axilla+triceps+subscap+suprailiac+abdomen+thigh
if (s === 0) return null
const D = sex === 'm'
? 1.112 - 0.00043499*s + 0.00000055*s*s - 0.00028826*age
: 1.097 - 0.00046971*s + 0.00000056*s*s - 0.00012828*age
return siri(D)
}
if (method === 'durnin') {
const { biceps=0, triceps=0, subscap=0, suprailiac=0 } = skinfolds
const s = biceps+triceps+subscap+suprailiac
if (s === 0) return null
const logS = Math.log10(s)
const tbl = sex === 'm' ? [
[20,1.1620,0.0630],[30,1.1631,0.0632],[40,1.1422,0.0544],[50,1.1620,0.0700],[99,1.1715,0.0779]
] : [
[20,1.1549,0.0678],[30,1.1599,0.0717],[40,1.1423,0.0632],[50,1.1333,0.0612],[99,1.1339,0.0645]
]
const [, c, m] = tbl.find(([maxAge]) => age < maxAge) || tbl[tbl.length-1]
return siri(c - m * logS)
}
if (method === 'parrillo') {
const sum = Object.values(skinfolds).reduce((a,b) => a+(b||0), 0)
return Math.max(3, Math.min(50, (sum * 27) / 1000))
}
return null
}
export const METHOD_POINTS = {
jackson3: { m: ['chest','abdomen','thigh'], f: ['triceps','suprailiac','thigh'] },
jackson7: { m: ['chest','axilla','triceps','subscap','suprailiac','abdomen','thigh'],
f: ['chest','axilla','triceps','subscap','suprailiac','abdomen','thigh'] },
durnin: { m: ['biceps','triceps','subscap','suprailiac'], f: ['biceps','triceps','subscap','suprailiac'] },
parrillo: { m: ['chest','axilla','triceps','subscap','suprailiac','abdomen','thigh','calf_med','lowerback'],
f: ['chest','axilla','triceps','subscap','suprailiac','abdomen','thigh','calf_med','lowerback'] },
}
export const BF_CATEGORIES = {
m: [
{max:6, label:'Essenziell', color:'#378ADD', desc:'Unter diesem Wert sind lebenswichtige Fette betroffen.'},
{max:14, label:'Athletisch', color:'#1D9E75', desc:'Typisch für Leistungssportler sehr definiert.'},
{max:18, label:'Fit', color:'#639922', desc:'Gute Fitness, gesunder Bereich für aktive Menschen.'},
{max:25, label:'Durchschnitt', color:'#EF9F27', desc:'Normaler Bereich für die allgemeine Bevölkerung.'},
{max:100,label:'Übergewicht', color:'#D85A30', desc:'Erhöhtes Gesundheitsrisiko, Reduktion empfohlen.'},
],
f: [
{max:14, label:'Essenziell', color:'#378ADD', desc:'Unter diesem Wert sind lebenswichtige Fette betroffen.'},
{max:21, label:'Athletisch', color:'#1D9E75', desc:'Typisch für Leistungssportlerinnen sehr definiert.'},
{max:25, label:'Fit', color:'#639922', desc:'Gute Fitness, gesunder Bereich für aktive Frauen.'},
{max:32, label:'Durchschnitt', color:'#EF9F27', desc:'Normaler Bereich für die allgemeine Bevölkerung.'},
{max:100,label:'Übergewicht', color:'#D85A30', desc:'Erhöhtes Gesundheitsrisiko, Reduktion empfohlen.'},
],
}
export function getBfCategory(pct, sex) {
return BF_CATEGORIES[sex]?.find(c => pct <= c.max) || BF_CATEGORIES[sex]?.at(-1)
}
export function calcDerived(m, height) {
const out = {}
if (m.c_waist && m.c_hip) out.whr = Math.round(m.c_waist / m.c_hip * 100) / 100
if (m.c_waist && height) out.whtr = Math.round(m.c_waist / height * 100) / 100
if (m.lean_mass && height) out.ffmi = Math.round(m.lean_mass / ((height/100)**2) * 10) / 10
return out
}
export function getRuleBasedAssessment(current, previous, profile) {
const sex = profile?.sex || 'm'
const height = profile?.height || 178
const findings = []
if (current.body_fat_pct) {
const cat = getBfCategory(current.body_fat_pct, sex)
findings.push({
type: ['Athletisch','Fit'].includes(cat.label) ? 'good' : cat.label === 'Durchschnitt' ? 'info' : cat.label === 'Essenziell' ? 'warn' : 'bad',
icon: '🔥', text: `Körperfett: ${current.body_fat_pct}% ${cat.label}`, detail: cat.desc
})
if (previous?.body_fat_pct) {
const delta = Math.round((current.body_fat_pct - previous.body_fat_pct) * 10) / 10
if (Math.abs(delta) >= 0.5) findings.push({
type: delta < 0 ? 'good' : 'warn', icon: delta < 0 ? '📉' : '📈',
text: `Körperfett ${delta > 0?'+':''}${delta}% seit letzter Messung`,
detail: delta < 0 ? 'Positive Entwicklung weiter so!' : 'Leichter Anstieg Ernährung und Training überprüfen.'
})
}
}
if (current.lean_mass && previous?.lean_mass) {
const delta = Math.round((current.lean_mass - previous.lean_mass) * 10) / 10
if (Math.abs(delta) >= 0.3) findings.push({
type: delta > 0 ? 'good' : 'warn', icon: delta > 0 ? '💪' : '⚠️',
text: `Magermasse ${delta > 0?'+':''}${delta} kg`,
detail: delta > 0 ? 'Muskelaufbau detektiert Training zeigt Wirkung.' : 'Rückgang der Magermasse auf ausreichend Protein und Krafttraining achten.'
})
}
if (current.lean_mass && height) {
const ffmi = Math.round(current.lean_mass / ((height/100)**2) * 10) / 10
const limit = sex === 'm' ? 25 : 22
findings.push({
type: ffmi >= 20 && ffmi <= limit ? 'good' : ffmi < 17 ? 'info' : 'warn',
icon: '📐', text: `FFMI: ${ffmi}`,
detail: ffmi < 17 ? 'Unterdurchschnittliche Muskelmasse Krafttraining empfohlen.'
: ffmi < 20 ? 'Durchschnittliche Muskelmasse.'
: ffmi < 23 ? 'Gute Muskelmasse gut trainierter Körper.'
: ffmi <= limit ? 'Sehr hohe Muskelmasse Leistungssportler-Niveau.'
: `Sehr hoher FFMI bei Werten über ${limit} kritisch hinterfragen.`
})
}
if (current.c_waist && current.c_hip) {
const whr = Math.round(current.c_waist / current.c_hip * 100) / 100
const limit = sex === 'm' ? 0.90 : 0.85
findings.push({
type: whr < limit ? 'good' : whr < limit+0.05 ? 'warn' : 'bad',
icon: '⚖️', text: `Waist-Hip-Ratio: ${whr} (Ziel: <${limit})`,
detail: whr < limit ? 'Gesunde Fettverteilung kein erhöhtes kardiovaskuläres Risiko.'
: whr < limit+0.05 ? 'Grenzwertiger Bereich Taillenumfang reduzieren empfohlen.'
: 'Erhöhtes kardiovaskuläres Risiko durch abdominelle Fettverteilung.'
})
}
if (current.c_waist && height) {
const whtr = Math.round(current.c_waist / height * 100) / 100
findings.push({
type: whtr < 0.50 ? 'good' : whtr < 0.60 ? 'warn' : 'bad',
icon: '📏', text: `Waist-to-Height-Ratio: ${whtr} (Ziel: <0,50)`,
detail: whtr < 0.50 ? 'Optimales Verhältnis Taille halb so groß wie Körpergröße.'
: whtr < 0.60 ? 'Leicht erhöhtes Risiko Taillenumfang sollte reduziert werden.'
: 'Deutlich erhöhtes Gesundheitsrisiko ärztliche Beratung empfohlen.'
})
}
if (current.c_waist) {
const limit = sex === 'm' ? 94 : 80
const limitHigh = sex === 'm' ? 102 : 88
if (current.c_waist > limit) findings.push({
type: current.c_waist > limitHigh ? 'bad' : 'warn',
icon: '🔴', text: `Taillenumfang ${current.c_waist} cm (WHO-Grenzwert: ${limit} cm)`,
detail: current.c_waist > limitHigh
? `Stark erhöhtes Risiko (WHO: über ${limitHigh} cm = hohes Risiko).`
: `Leicht erhöhtes metabolisches Risiko laut WHO-Kriterien.`
})
if (previous?.c_waist) {
const delta = Math.round((current.c_waist - previous.c_waist) * 10) / 10
if (Math.abs(delta) >= 1) findings.push({
type: delta < 0 ? 'good' : 'warn', icon: delta < 0 ? '✅' : '📊',
text: `Taille ${delta > 0?'+':''}${delta} cm seit letzter Messung`,
detail: delta < 0 ? 'Taillenumfang nimmt ab gute Entwicklung!' : 'Taille ist gewachsen Ernährung überprüfen.'
})
}
}
if (current.weight && previous?.weight) {
const delta = Math.round((current.weight - previous.weight) * 10) / 10
if (Math.abs(delta) >= 0.3) findings.push({
type: 'info', icon: '⚖️',
text: `Gewicht ${delta > 0?'+':''}${delta} kg seit letzter Messung`,
detail: 'Kombination mit Körperfett-Verlauf beachten Gewicht allein ist wenig aussagekräftig.'
})
}
const goods = findings.filter(f => f.type === 'good').length
const bads = findings.filter(f => f.type === 'bad').length
const warns = findings.filter(f => f.type === 'warn').length
let summary, summaryType
if (findings.length === 0) {
summary = 'Zu wenig Daten. Bitte Umfänge und Körperfett ergänzen.'; summaryType = 'info'
} else if (bads >= 2) {
summary = 'Mehrere Werte im kritischen Bereich gezielte Maßnahmen empfohlen.'; summaryType = 'bad'
} else if (bads === 1 || warns >= 2) {
summary = 'Einige Werte außerhalb des optimalen Bereichs Verbesserungspotenzial vorhanden.'; summaryType = 'warn'
} else if (goods >= 2) {
summary = 'Gute Körperzusammensetzung weiter so!'; summaryType = 'good'
} else {
summary = 'Werte im normalen Bereich regelmäßig weiter messen.'; summaryType = 'info'
}
return { findings, summary, summaryType }
}
export const CIRCUMFERENCE_GUIDE = [
{ id:'c_neck', name:'Hals', color:'#1D9E75',
where:'Direkt unterhalb des Adamsapfels, schmalste Stelle des Halses',
posture:'Gerade stehen, Kopf neutral, nicht nach vorne beugen',
how:'Waagerecht, 1 Finger Luft zwischen Band und Hals',
tip:'Morgens nüchtern messen für konsistente Werte' },
{ id:'c_chest', name:'Brust', color:'#378ADD',
where:'Breiteste Stelle des Brustkorbs, über den Brustmuskeln (Männer) bzw. vollsten Stelle der Brust (Frauen)',
posture:'Aufrecht, Arme locker seitlich am Ende normaler Ausatmung messen',
how:'Waagerecht, parallel zum Boden, fest aber nicht einschneidend',
tip:'Nicht einatmen beim Messen Werte ändern sich um bis zu 5 cm!' },
{ id:'c_waist', name:'Taille', color:'#EF9F27',
where:'Schmalste Stelle des Rumpfes meist 23 cm oberhalb des Bauchnabels',
posture:'Aufrecht, Bauch entspannen, Arme locker hängen lassen',
how:'Waagerecht, eng aber nicht zusammenpressend',
tip:'Beim seitlichen Beugen wird die schmalste Stelle gut sichtbar' },
{ id:'c_belly', name:'Bauch', color:'#D85A30',
where:'Exakt auf Höhe des Bauchnabels',
posture:'Stehend, Bauch vollständig entspannen nicht einziehen!',
how:'Waagerecht, ohne Druck',
tip:'Wichtigster Einzelwert für viszerales Fett und Gesundheitsrisiko' },
{ id:'c_hip', name:'Hüfte', color:'#D4537E',
where:'Breiteste Stelle des Gesäßes, ca. 1520 cm unterhalb des Bauchnabels',
posture:'Aufrecht, Füße zusammen, Gewicht gleichmäßig',
how:'Über die breiteste Stelle des Gesäßes, waagerecht',
tip:'Für WHR: Taille ÷ Hüfte (Ziel: <0,85 Frauen / <0,90 Männer)' },
{ id:'c_thigh', name:'Oberschenkel', color:'#7F77DD',
where:'Dickste Stelle des Oberschenkels, 5 cm unterhalb des Schritts',
posture:'Aufrecht, Gewicht gleichmäßig nicht auf ein Bein verlagern!',
how:'Waagerecht, immer rechte Seite, gleicher Abstand vom Schritt',
tip:'Mit Lineal vorher markieren für reproduzierbare Werte' },
{ id:'c_calf', name:'Wade', color:'#639922',
where:'Dickste Stelle der Wade, Mitte zwischen Knöchel und Kniebeuge',
posture:'Aufrecht, Gewicht gleichmäßig verteilt',
how:'Waagerecht, Muskel entspannt',
tip:'Morgens messen abends schwellen Beine durch Wassereinlagerungen an' },
{ id:'c_arm', name:'Oberarm', color:'#1D9E75',
where:'Dickste Stelle, Mitte zwischen Schultergelenk und Ellenbogen',
posture:'Arm locker hängen lassen und entspannen',
how:'Waagerecht, senkrecht zur Längsachse',
tip:'Immer denselben Arm (rechts); auch angespannt messen und notieren' },
]
export const CALIPER_GUIDE = {
chest: { name:'Brust', color:'#378ADD',
where:'Diagonale Falte, halb zwischen Achselhöhle und Brustwarze (Männer); 1/3 des Abstands (Frauen)',
posture:'Aufrecht, Arm leicht angehoben', how:'Diagonale Falte (45°)',
tip:'Liegt medial der Achselfalte nicht zu weit nach außen greifen' },
axilla: { name:'Achsel', color:'#D4537E',
where:'Mittlere Achsellinie, auf Höhe des Xiphoids (Brustbeinansatz)',
posture:'Arm leicht nach vorne', how:'Vertikale Falte',
tip:'Schwieriger Punkt Helfer sinnvoll' },
triceps: { name:'Trizeps', color:'#EF9F27',
where:'Rückseite Oberarm, Mitte zwischen Schultergelenk und Ellenbogen',
posture:'Arm hängt entspannt seitlich', how:'Vertikale Falte, parallel zur Längsachse',
tip:'Wichtigster Punkt in Frauen-Formeln Arm vollständig entspannen' },
subscap: { name:'Schulterblatt', color:'#7F77DD',
where:'12 cm unterhalb der unteren Schulterblatt-Ecke',
posture:'Arm hängt locker, leicht nach hinten', how:'Diagonale Falte (45°) entlang natürlicher Hautlinien',
tip:'Arm nach hinten halten lassen für besseren Zugang' },
suprailiac: { name:'Hüftkamm', color:'#D85A30',
where:'Direkt oberhalb des Hüftkamms (Crista iliaca), vordere Achsellinie',
posture:'Aufrecht, Arme leicht angehoben', how:'Diagonale Falte (45° nach innen-unten)',
tip:'Liegt ÜBER dem Hüftknochen nicht mit Bauch-Punkt verwechseln' },
abdomen: { name:'Bauch', color:'#D85A30',
where:'2 cm rechts neben dem Bauchnabel',
posture:'Stehend, Bauch entspannen', how:'Horizontale Falte',
tip:'Bauch vollständig entspannen nicht einziehen!' },
thigh: { name:'Oberschenkel', color:'#1D9E75',
where:'Vorderseite, Mitte zwischen Leiste und Kniescheibe',
posture:'Gewicht auf linkes Bein verlagern (rechter Muskel entspannt)', how:'Vertikale Falte',
tip:'Gewicht aufs andere Bein Muskel muss entspannt sein' },
calf_med: { name:'Wade (medial)', color:'#639922',
where:'Innenseite der Wade, dickste Stelle',
posture:'Sitzend, Fuß flach, Knie 90°', how:'Vertikale Falte',
tip:'Bein vollständig entspannen' },
biceps: { name:'Bizeps', color:'#1D9E75',
where:'Vorderseite Oberarm, Mitte zwischen Schultergelenk und Ellenbogen',
posture:'Arm hängt entspannt', how:'Vertikale Falte',
tip:'Nur Durnin-Methode' },
lowerback: { name:'Lendenwirbel', color:'#888780',
where:'Über Lendenwirbelsäule, 2 cm seitlich der Mittellinie auf Höhe L4',
posture:'Leicht nach vorne gebeugt', how:'Horizontale Falte',
tip:'Helfer bitten schwer allein erreichbar' },
}