280 lines
15 KiB
JavaScript
280 lines
15 KiB
JavaScript
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 2–3 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. 15–20 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:'1–2 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' },
|
||
}
|