🔬 Control de Calidad ISAK · Error Técnico de Medición (TEM)Umbrales: Pliegues ≤5% · Perímetros ≤1% · Diámetros/Longitudes ≤0.5%
📊
Diagnóstico Final
Composición corporal · Protocolo KERR · Ajuste exacto al peso real
Indicadores Clave
—
Peso Real
—
Suma Final
—
Ajuste Aplicado
* Los valores han sido ajustados proporcionalmente para coincidir al 100% con el peso de báscula
Composición por Componente
—
kg total
📁 Historial de Evaluaciones
Selecciona 2 para comparar
📈 Evolución
⚖ Comparación de Evaluaciones
📚 Biblioteca de Papers · Paper + Podcast + Infografía
📊 Mi Práctica · Dashboard agregado
⚙️ Sistema · Configuración avanzada
🔌 Importar datos BIA
Pega los datos exportados desde tu balanza BIA (Tanita BC-545/BC-601, InBody 270/770, Seca mBCA, OMRON, Withings, etc.). El sistema detecta automáticamente el formato (CSV, JSON o key=value) y mapea los campos.
Tu somatotipo (${somato.endo.toFixed(1)}–${somato.meso.toFixed(1)}–${somato.ecto.toFixed(1)}) comparado con valores élite por deporte (Carter & Heath):
⚖️ Este informe es de naturaleza antropométrica y no constituye diagnóstico médico. Las conclusiones son orientativas y deben ser interpretadas por el profesional tratante en el contexto clínico individual.
✅ Sin necesidad de derivación profesional adicional según los datos evaluados.
`}
`;
document.getElementById('advanced-results').innerHTML = html;
// Activate indices first tab + animate counters again (for the new bento)
setTimeout(() => { setIdxTab('imo'); animateAllCounters(); }, 50);
}
// Indices tab switcher
function setIdxTab(key) {
if (!window._indices6 || !window._indices6[key]) return;
document.querySelectorAll('.idx-tab').forEach(el => el.classList.toggle('active', el.dataset.idx === key));
const i = window._indices6[key];
const panel = document.getElementById('idx-panel');
if (!panel) return;
// Re-trigger pop animation
panel.classList.remove('pop');
void panel.offsetWidth;
panel.classList.add('pop');
panel.innerHTML = `
${escapeHtml(i.label)} · ${key.toUpperCase()}
${i.val.toFixed(2)}
${escapeHtml(i.classify)}
${escapeHtml(i.interp)}
📊 ${escapeHtml(i.ref)}
`;
}
// ════════════════════════════════════════════════════════════
// CLASSIFICATIONS
// ════════════════════════════════════════════════════════════
function clasificarGrasa(pct, sexo) {
const m = sexo === 'masculino';
if (pct < (m ? 6 : 14)) return [t('norm.essential'),'bg-amber'];
if (pct < (m ? 14 : 21)) return [t('norm.athletic'),'bg-blue'];
if (pct < (m ? 18 : 25)) return [t('norm.fitness'),'bg-green'];
if (pct < (m ? 25 : 32)) return [t('norm.acceptable'),'bg-amber'];
return [t('norm.obese'),'bg-red'];
}
function calcSomatotype(pctAdip, pctMusc, sexo) {
const highFat = sexo === 'masculino' ? pctAdip > 22 : pctAdip > 28;
const highMusc = pctMusc > 46;
const lowFat = sexo === 'masculino' ? pctAdip < 11 : pctAdip < 18;
if (highFat && !highMusc) return { icon:'🟠', name:t('soma.endo'), desc:t('soma.endoDesc') };
if (highMusc && !highFat) return { icon:'🔵', name:t('soma.meso'), desc:t('soma.mesoDesc') };
if (lowFat && !highMusc) return { icon:'🟡', name:t('soma.ecto'), desc:t('soma.ectoDesc') };
return { icon:'🟢', name:t('soma.inter'), desc:t('soma.interDesc') };
}
// ════════════════════════════════════════════════════════════
// ISAK — Heath-Carter Somatotype (3 components)
// ════════════════════════════════════════════════════════════
function heathCarter(M) {
// M is the measurements object: triceps, subescapular, supraespinal,
// abdominal, pliegueMuslo, plieguePierna, brazoFlex, pierna, humero,
// femur, peso, talla
const stature = M.talla;
const corr = 170.18 / stature;
// Endomorphy
const X = (M.triceps + M.subescapular + M.supraespinal) * corr;
const endo = -0.7182 + 0.1451*X - 0.00068*X*X + 0.0000014*X*X*X;
// Mesomorphy
const cArm = M.brazoFlex - (M.triceps / 10);
const cCalf = M.pierna - (M.plieguePierna / 10);
const meso = 0.858*M.humero + 0.601*M.femur + 0.188*cArm + 0.161*cCalf - 0.131*stature + 4.5;
// Ectomorphy via HWR
const hwr = stature / Math.pow(M.peso, 1/3);
let ecto;
if (hwr >= 40.75) ecto = 0.732 * hwr - 28.58;
else if (hwr >= 38.25) ecto = 0.463 * hwr - 17.63;
else ecto = 0.1;
return {
endo: Math.max(0.1, endo),
meso: Math.max(0.1, meso),
ecto: Math.max(0.1, ecto),
hwr
};
}
function somatoInterpretation(comp, value) {
// comp: 'endo'|'meso'|'ecto', value: number
const lvl = value < 2.5 ? 'low' : value < 5 ? 'mid' : value < 7 ? 'high' : 'veryhigh';
const map = {
endo: {
low: 'Low relative fatness; little subcutaneous fat.',
mid: 'Moderate relative fatness; average subcutaneous fat.',
high: 'High relative fatness; thick subcutaneous fat; increased storage of fat in abdomen.',
veryhigh: 'Extremely high relative fatness; very thick subcutaneous fat.'
},
meso: {
low: 'Low relative musculo-skeletal development; thin muscles; narrow skeleton.',
mid: 'Moderate musculo-skeletal development; average muscle bulk and bone size.',
high: 'High relative musculo-skeletal development; bulky muscles; wide skeleton and joints.',
veryhigh: 'Extremely high relative musculo-skeletal development; very bulky muscles; very wide skeleton and joints.'
},
ecto: {
low: 'Low relative linearity; great bulk per unit of height; round like a ball; relatively bulky limbs.',
mid: 'Moderate relative linearity; balanced ratio between height and bulk.',
high: 'High relative linearity; long thin limbs; lean trunk; little subcutaneous tissue.',
veryhigh: 'Extremely linear; long fragile limbs; very low bulk per unit of height.'
}
};
return map[comp][lvl];
}
// ════════════════════════════════════════════════════════════
// ISAK — Durnin-Womersley body fat (1974)
// ════════════════════════════════════════════════════════════
function durninWomersley(M, sexo, edad) {
// Sum of 4: triceps + biceps + subscapular + suprailiac (iliac crest)
const sum4 = M.triceps + M.biceps + M.subescapular + M.crestaIliaca;
if (sum4 <= 0) return { density: 0, fatPct: 0, fatMass: 0, ffMass: M.peso };
const log = Math.log10(sum4);
// Coefficients by age & sex (Durnin & Womersley 1974, Br J Nutr)
const COEF = {
masculino: [
{ max: 19, c: 1.1620, m: 0.0630 },
{ max: 29, c: 1.1631, m: 0.0632 },
{ max: 39, c: 1.1422, m: 0.0544 },
{ max: 49, c: 1.1620, m: 0.0700 },
{ max: 200, c: 1.1715, m: 0.0779 }
],
femenino: [
{ max: 19, c: 1.1549, m: 0.0678 },
{ max: 29, c: 1.1599, m: 0.0717 },
{ max: 39, c: 1.1423, m: 0.0632 },
{ max: 49, c: 1.1333, m: 0.0612 },
{ max: 200, c: 1.1339, m: 0.0645 }
]
};
const tbl = COEF[sexo] || COEF.masculino;
const row = tbl.find(r => edad <= r.max) || tbl[tbl.length-1];
const density = row.c - row.m * log;
const fatPct = (4.95 / density - 4.5) * 100;
const fatMass = (fatPct / 100) * M.peso;
return { density, fatPct, fatMass, ffMass: M.peso - fatMass };
}
// ════════════════════════════════════════════════════════════
// ISAK — Health, proportionality and body comp indices
// ════════════════════════════════════════════════════════════
function healthIndices(M, sexo) {
const peso = M.peso, talla = M.talla;
const cintura = M.cintura, cadera = M.cadera;
const imc = peso / Math.pow(talla/100, 2);
const whr = cadera ? cintura / cadera : 0;
const whtr = talla ? cintura / talla : 0;
// Conicity index (Valdez 1991): waist(m) / (0.109 × √(weight/height(m)))
const ci = (cintura/100) / (0.109 * Math.sqrt(peso / (talla/100)));
// Fat distribution index = limbs / trunk (skinfolds)
const limbs = M.triceps + M.pliegueMuslo + M.plieguePierna;
const trunk = M.subescapular + M.supraespinal + M.abdominal;
const fdi = trunk > 0 ? limbs / trunk : 0;
// BMI classification
let bmiCls;
if (imc < 18.5) bmiCls = { txt: 'Underweight', light: 'amber' };
else if (imc < 25) bmiCls = { txt: 'Normal', light: 'green' };
else if (imc < 30) bmiCls = { txt: 'Overweight', light: 'amber' };
else if (imc < 35) bmiCls = { txt: 'Grade 1 Obesity', light: 'red' };
else if (imc < 40) bmiCls = { txt: 'Grade 2 Obesity', light: 'red' };
else bmiCls = { txt: 'Grade 3 Obesity', light: 'red' };
// WHR classification (sex-specific)
const whrLimit = sexo === 'masculino' ? 1.00 : 0.85;
const whrCls = whr === 0 ? null
: { txt: whr < whrLimit ? 'Low risk of developing chronic diseases' : 'High risk of developing chronic diseases',
light: whr < whrLimit ? 'green' : 'red' };
// WHtR
const whtrCls = whtr === 0 ? null
: { txt: whtr < 0.5 ? 'Low risk of developing chronic diseases' : 'High risk of developing chronic diseases',
light: whtr < 0.5 ? 'green' : 'red' };
// Conicity
const ciLimit = sexo === 'masculino' ? 1.25 : 1.18;
const ciCls = !cintura ? null
: { txt: ci < ciLimit ? 'Low cardiovascular risk' : 'High cardiovascular risk',
light: ci < ciLimit ? 'green' : 'amber' };
return {
imc, whr, whtr, ci, fdi,
limbs, trunk,
limbsPct: (limbs+trunk)>0 ? (limbs/(limbs+trunk))*100 : 0,
trunkPct: (limbs+trunk)>0 ? (trunk/(limbs+trunk))*100 : 0,
bmiCls, whrCls, whtrCls, ciCls,
whrLimit, ciLimit
};
}
function bodyCompIndices(tissues, M, sexo) {
const ami = tissues.muscle ? tissues.adipose / tissues.muscle : 0;
const mbi = tissues.bone ? tissues.muscle / tissues.bone : 0;
// AMI classification
let amiCls;
if (ami < 0.4) amiCls = 'Low';
else if (ami < 0.7) amiCls = 'Normal';
else if (ami < 1.0) amiCls = 'High';
else amiCls = 'Very high';
// MBI classification (rough)
let mbiCls;
if (mbi < 3.5) mbiCls = 'Low';
else if (mbi < 5.5) mbiCls = 'Normal';
else mbiCls = 'High';
return { ami, amiCls, mbi, mbiCls };
}
function proportionalityIndices(M, sexo) {
const cormic = M.estaturaSentado && M.talla ? (M.estaturaSentado / M.talla) * 100 : 0;
const acIliac = M.biacromial ? (M.biiliocrestal / M.biacromial) * 100 : 0;
let cormicCls;
if (cormic < 51) cormicCls = 'Long-legged trunk';
else if (cormic < 53) cormicCls = 'Proportionate trunk at shoulders and hips';
else cormicCls = 'Short-legged trunk';
// Acromio-iliac classification (sex-specific approximate)
let aiCls;
const lo = sexo === 'masculino' ? 70 : 73;
const hi = sexo === 'masculino' ? 75 : 78;
if (acIliac < lo) aiCls = 'Hips proportionally narrower than shoulders';
else if (acIliac < hi) aiCls = 'Proportionate at shoulders and hips';
else aiCls = 'Hips proportionally wider than shoulders';
return { cormic, cormicCls, acIliac, aiCls };
}
// ════════════════════════════════════════════════════════════
// ADVANCED MODULES — energetics, multi-formula BF, goals, athletes
// ════════════════════════════════════════════════════════════
// BMR / TDEE — 8 fórmulas (paridad con 5componentes)
function energyExpenditure(M, sexo, edad, ffMass) {
const W = M.peso, H = M.talla, A = edad, m = sexo==='masculino';
// 1. Mifflin-St Jeor (1990)
const mifflin = m ? 10*W + 6.25*H - 5*A + 5
: 10*W + 6.25*H - 5*A - 161;
// 2. Harris-Benedict revisado (Roza 1984)
const harris = m ? 88.362 + 13.397*W + 4.799*H - 5.677*A
: 447.593 + 9.247*W + 3.098*H - 4.330*A;
// 3. Katch-McArdle (usa FFM, ideal para atletas)
const katch = ffMass > 0 ? 370 + 21.6 * ffMass : null;
// 4. Cunningham (usa FFM, también muy usada en atletas)
const cunningham = ffMass > 0 ? 500 + 22 * ffMass : null;
// 5. Oxford / Henry (2005) — por rangos etarios
let oxford;
if (m) {
if (A < 18) oxford = 16.25*W + 137.2*H/100 + 515.5;
else if (A < 30) oxford = 14.4*W + 313*H/100 + 113;
else if (A < 60) oxford = 11.4*W + 541*H/100 - 137;
else oxford = 11.4*W + 541*H/100 - 256;
} else {
if (A < 18) oxford = 8.365*W + 465*H/100 + 200;
else if (A < 30) oxford = 10.4*W + 615*H/100 - 282;
else if (A < 60) oxford = 8.18*W + 502*H/100 - 11.6;
else oxford = 8.52*W + 421*H/100 + 10.7;
}
// 6. FAO/OMS (1985)
let faoOms;
if (m) {
if (A < 18) faoOms = 17.5*W + 651;
else if (A < 30) faoOms = 15.3*W + 679;
else if (A < 60) faoOms = 11.6*W + 879;
else faoOms = 13.5*W + 487;
} else {
if (A < 18) faoOms = 12.2*W + 746;
else if (A < 30) faoOms = 14.7*W + 496;
else if (A < 60) faoOms = 8.7*W + 829;
else faoOms = 10.5*W + 596;
}
// 7. Valencia (Carrasco 2002) — población chilena
let valencia;
if (m) {
if (A < 30) valencia = 15.3*W + 679;
else if (A < 60) valencia = 11.6*W + 879;
else valencia = 13.5*W + 487;
} else {
if (A < 30) valencia = 14.7*W + 496;
else if (A < 60) valencia = 8.7*W + 829;
else valencia = 10.5*W + 596;
}
// 8. Owen (1986/1987)
const owen = m ? 879 + 10.2 * W : 795 + 7.18 * W;
const list = [
{ name: 'Mifflin-St Jeor', val: mifflin, color:'#0090CC' },
{ name: 'Harris-Benedict', val: harris, color:'#7c3aed' },
{ name: 'Katch-McArdle', val: katch, color:'#16a34a' },
{ name: 'Cunningham', val: cunningham, color:'#f97316' },
{ name: 'Oxford / Henry', val: oxford, color:'#dc2626' },
{ name: 'FAO/OMS', val: faoOms, color:'#0e7490' },
{ name: 'Valencia (Chile)', val: valencia, color:'#be185d' },
{ name: 'Owen', val: owen, color:'#a16207' }
];
const vals = list.map(x => x.val).filter(v => v != null && isFinite(v) && v > 0);
const avg = vals.length ? vals.reduce((a,b)=>a+b,0) / vals.length : 0;
return {
list,
avg,
bmrMifflin: mifflin, // backwards-compat aliases
bmrHarris: harris,
bmrCunningham: cunningham,
bmrAvg: avg
};
}
// ════════════════════════════════════════════════════════════
// Total Body Water (TBW) — 5 fórmulas
// ════════════════════════════════════════════════════════════
function multiTbwFormulas(M, sexo, edad) {
if (!M.peso || !M.talla) return null;
const W = M.peso, H = M.talla, A = edad, m = sexo==='masculino';
// Watson (1980)
const watson = m
? 2.447 - 0.09516*A + 0.1074*H + 0.3362*W
: -2.097 + 0.1069*H + 0.2466*W;
// Hume-Weyers (1971)
const hume = m
? 0.194786*H + 0.296785*W - 14.012934
: 0.34454*H + 0.183809*W - 35.270121;
// Chertow (1997) — diálisis general
const chertow = m
? -28.3497 + 0.1844*H + 0.3232*W - 0.0681*A + 0.0006 * (W*W)
: -26.3933 + 0.2862*W + 0.1715*H - 0.0378*A + 0.0007 * (W*W);
// Chertow (1997) - diabetes adjustment
const chertowDb = chertow - 1.07;
// Promedio 3 fórmulas (Watson + Hume + Chertow)
const mean3 = (watson + hume + chertow) / 3;
const list = [
{ name: 'Watson (1980)', val: watson, color:'#a16207' },
{ name: 'Hume-Weyers (1971)', val: hume, color:'#f97316' },
{ name: 'Chertow (1997)', val: chertow, color:'#16a34a' },
{ name: 'Chertow (diabetes)', val: chertowDb, color:'#eab308' },
{ name: 'Media 3 fórmulas', val: mean3, color:'#0090CC' }
];
return {
list,
pctList: list.map(x => ({ ...x, pct: (x.val / W) * 100 })),
avgL: mean3,
avgPct: (mean3 / W) * 100,
// OMS recommended water intake: 35 mL/kg base, athletes 40-45
intakeOMS_ml: Math.round(W * 35),
intakeAthlete_ml: Math.round(W * 45)
};
}
// ════════════════════════════════════════════════════════════
// Body Composition Indices (IMO, ICA, ISA, IMG, IMM, IAM)
// Paridad con 5componentes
// ════════════════════════════════════════════════════════════
function bodyCompositionIndices6(M, tissues, dwResult, leeResult) {
const W = M.peso, H = M.talla;
const htM = H / 100;
const sum6 = M.triceps + M.subescapular + M.supraespinal + M.abdominal + M.pliegueMuslo + M.plieguePierna;
// IMO - Índice Músculo-Óseo (kg musc / kg ósea)
const imo = tissues.bone ? tissues.muscle / tissues.bone : 0;
// ICA - Índice Cintura-Altura (cintura/talla en cm)
const ica = H && M.cintura ? M.cintura / H : 0;
// ISA - Índice Sumatoria/Altura (6 pliegues en mm / talla en cm × 100)
const isa = H ? (sum6 / H) * 100 : 0;
// IMG - Índice de Masa Grasa (kg grasa / m²)
const fatMass = dwResult ? dwResult.fatMass : tissues.adipose;
const img = htM ? fatMass / (htM*htM) : 0;
// IMM - Índice de Masa Muscular esquelética (Lee SMM o KERR muscular / m²)
const muscleKg = leeResult ? leeResult.smm : tissues.muscle;
const imm = htM ? muscleKg / (htM*htM) : 0;
// IAM - Índice Adipose-Muscular (kg grasa / kg músculo)
const iam = tissues.muscle ? tissues.adipose / tissues.muscle : 0;
return {
imo: { val: imo, label: 'Índice Músculo-Óseo (IMO)',
interp: 'kg de masa muscular por cada kg de masa ósea. Indica cuánto músculo soporta el esqueleto.',
classify: imo < 3.8 ? 'Bajo' : imo < 4.9 ? 'Normal' : 'Alto',
ref: 'ARGOREF: bajo <3.8 · normal 3.8-4.9 · alto >4.9' },
ica: { val: ica, label: 'Índice Cintura-Altura (ICA)',
interp: 'Riesgo cardiometabólico abdominal. Valor único para ambos sexos.',
classify: ica < 0.5 ? 'Saludable' : ica < 0.6 ? 'Sobrepeso/riesgo' : 'Riesgo elevado',
ref: 'Saludable <0.50 · Riesgo 0.50-0.60 · Elevado >0.60' },
isa: { val: isa, label: 'Índice Sumatoria-Altura (ISA)',
interp: 'Suma de 6 pliegues normalizada por talla. Indicador de adiposidad relativa.',
classify: isa < 50 ? 'Bajo' : isa < 100 ? 'Normal' : 'Alto',
ref: 'Bajo <50 · Normal 50-100 · Alto >100' },
img: { val: img, label: 'Índice de Masa Grasa (IMG)',
interp: 'kg de grasa por m². Análogo al IMC pero específico para grasa.',
classify: img < 4 ? 'Bajo' : img < 8 ? 'Normal' : img < 12 ? 'Sobrepeso' : 'Obesidad',
ref: 'Bajo <4 · Normal 4-8 · Sobrepeso 8-12 · Obesidad >12' },
imm: { val: imm, label: 'Índice de Masa Muscular (IMM)',
interp: 'kg de músculo esquelético por m² (Lee). Detecta sarcopenia.',
classify: imm < 6.76 ? 'Bajo / sarcopenia' : imm < 8.5 ? 'Normal' : 'Alto',
ref: 'Sarcopenia <6.76 · Normal 6.76-8.5 · Alto >8.5 (mujeres; ajustar +30% en hombres)' },
iam: { val: iam, label: 'Índice Adipose-Muscular (IAM)',
interp: 'kg grasa que cada kg de músculo debe transportar. Menor = mejor eficiencia.',
classify: iam < 0.4 ? 'Bajo' : iam < 0.7 ? 'Normal' : iam < 1.0 ? 'Alto' : 'Muy alto',
ref: 'Bajo <0.4 · Normal 0.4-0.7 · Alto 0.7-1.0 · Muy alto >1.0' }
};
}
function tdeeAndMacros(bmr, activityFactor, goalPct, weight, goalType) {
const tdee = bmr * activityFactor;
const target = tdee * (1 + goalPct/100);
// Macro split by goal
let pPct, cPct, fPct;
if (goalPct < -10) { pPct=40; cPct=35; fPct=25; } // aggressive loss
else if (goalPct < 0) { pPct=35; cPct=40; fPct=25; } // moderate loss
else if (goalPct === 0) { pPct=25; cPct=50; fPct=25; } // maintenance
else { pPct=30; cPct=50; fPct=20; } // gain
const protG = (target * pPct/100) / 4;
const carbG = (target * cPct/100) / 4;
const fatG = (target * fPct/100) / 9;
return {
tdee, target,
protein: { pct: pPct, kcal: target*pPct/100, grams: protG, gPerKg: protG/weight },
carbs: { pct: cPct, kcal: target*cPct/100, grams: carbG, gPerKg: carbG/weight },
fat: { pct: fPct, kcal: target*fPct/100, grams: fatG, gPerKg: fatG/weight },
waterMl: Math.round(weight * 35) // base hydration target
};
}
// ════════════════════════════════════════════════════════════
// Lee et al. (2000) — Skeletal Muscle Mass (SMM) via anthropometry
// Validated vs MRI in non-obese adults. Uses ethnicity adjustment.
// ════════════════════════════════════════════════════════════
function leeSMM(M, sexo, edad, etnia) {
if (!M.peso || !M.talla || !M.brazo || !M.musloG || !M.pierna) return null;
const PI = Math.PI;
const cArm = M.brazo - (PI * M.triceps / 10);
const cThigh = M.musloG - (PI * M.pliegueMuslo / 10);
const cCalf = M.pierna - (PI * M.plieguePierna / 10);
const htM = M.talla / 100;
const sex = sexo === 'masculino' ? 1 : 0;
let race = 0;
if (etnia === 'asian') race = -2.0;
else if (etnia === 'african') race = 1.1;
// SM (kg) = Ht × (0.00744·CAG² + 0.00088·CTG² + 0.00441·CCG²) + 2.4·Sex − 0.048·Age + Race + 7.8
const smm = htM * (0.00744 * cArm * cArm + 0.00088 * cThigh * cThigh + 0.00441 * cCalf * cCalf)
+ 2.4 * sex - 0.048 * edad + race + 7.8;
// Sanity bounds
if (!isFinite(smm) || smm <= 0 || smm > 100) return null;
return {
smm,
smmPct: (smm / M.peso) * 100,
smi: smm / (htM * htM), // Skeletal Muscle Mass Index (Janssen 2002)
components: { cArm, cThigh, cCalf },
classification: classifySMI(smm / (htM * htM), sexo)
};
}
function classifySMI(smi, sexo) {
// Janssen (2002) cutoffs for sarcopenia
const m = sexo === 'masculino';
if (smi >= (m ? 10.76 : 6.76)) return { label: 'Normal', light: 'green' };
if (smi >= (m ? 8.51 : 5.76)) return { label: 'Sarcopenia clase I', light: 'amber' };
return { label: 'Sarcopenia clase II', light: 'red' };
}
// ════════════════════════════════════════════════════════════
// Body Surface Area (DuBois 1916)
// ════════════════════════════════════════════════════════════
function bodySurfaceArea(W, H) {
if (!W || !H) return 0;
return 0.007184 * Math.pow(W, 0.425) * Math.pow(H, 0.725);
}
// ════════════════════════════════════════════════════════════
// Sum of 7 skinfolds (Jackson-Pollock)
// ════════════════════════════════════════════════════════════
function sum7Skinfolds(M) {
// Classic JP7: chest, axillary, triceps, subscapular, abdominal, suprailiac, thigh
// We don't have chest skinfold — use the sum of available 7 closest sites
return M.triceps + M.subescapular + M.supraespinal + M.abdominal + M.pliegueMuslo + (M.axilarMedial || M.plieguePierna) + (M.crestaIliaca || M.biceps);
}
// ════════════════════════════════════════════════════════════
// 2-Component model (Siri / Brozek from body density)
// ════════════════════════════════════════════════════════════
function twoComponentModel(M, dwResult) {
if (!dwResult) return null;
const fatMass = dwResult.fatMass;
const ffMass = M.peso - fatMass;
return {
fatMass, ffMass,
fatPct: (fatMass / M.peso) * 100,
ffPct: (ffMass / M.peso) * 100,
density: dwResult.density,
formula: 'Siri 1961 / Durnin-Womersley 1974'
};
}
// ════════════════════════════════════════════════════════════
// 4-Component model (simplified: water + bone mineral + fat + residual)
// TBW via Watson 1980 equation
// ════════════════════════════════════════════════════════════
function fourComponentModel(M, sexo, edad, dwResult, tissues) {
if (!dwResult || !tissues) return null;
const W = M.peso, H = M.talla;
// Watson TBW (L)
let tbw;
if (sexo === 'masculino') tbw = 2.447 - 0.09516*edad + 0.1074*H + 0.3362*W;
else tbw = -2.097 + 0.1069*H + 0.2466*W;
// Bone mineral ≈ 50% of osseous tissue (Kerr)
const bmc = tissues.bone * 0.5;
// Fat from DW
const fat = dwResult.fatMass;
// Protein (residual) = weight − fat − water − bone mineral
const protein = W - fat - tbw - bmc;
return {
fat, water: tbw, bone: bmc, protein,
fatPct: (fat/W)*100,
waterPct: (tbw/W)*100,
bonePct: (bmc/W)*100,
proteinPct: (protein/W)*100,
formula: 'Watson 1980 + Kerr 1991 + DW 1974'
};
}
// ════════════════════════════════════════════════════════════
// IMC DIAL GAUGE (semicircular, color-zone SVG)
// ════════════════════════════════════════════════════════════
function imcDialSvg(imc) {
// Map imc 10..50 → angle -90..+90 deg (semicircle)
const min = 10, max = 50;
const v = Math.max(min, Math.min(max, imc));
const angle = (-90 + ((v - min) / (max - min)) * 180); // degrees
const rad = angle * Math.PI / 180;
// Needle tip at radius 75 from center (100,100)
const cx = 100, cy = 100, r = 75;
const nx = cx + r * Math.cos(rad - Math.PI/2);
const ny = cy + r * Math.sin(rad - Math.PI/2);
// Helper: polar arc segment
const arc = (startV, endV, color) => {
const a1 = (-90 + ((startV - min) / (max - min)) * 180) * Math.PI / 180;
const a2 = (-90 + ((endV - min) / (max - min)) * 180) * Math.PI / 180;
const x1 = cx + 85 * Math.cos(a1 - Math.PI/2);
const y1 = cy + 85 * Math.sin(a1 - Math.PI/2);
const x2 = cx + 85 * Math.cos(a2 - Math.PI/2);
const y2 = cy + 85 * Math.sin(a2 - Math.PI/2);
const large = (endV - startV) / (max - min) * 180 > 180 ? 1 : 0;
return ``;
};
// Zones (BMI ISO)
const zones = arc(10, 18.5, '#3b82f6') // bajo peso (azul)
+ arc(18.5, 25, '#16a34a') // normal (verde)
+ arc(25, 30, '#eab308') // sobrepeso (amarillo)
+ arc(30, 35, '#f97316') // obesidad 1 (naranja)
+ arc(35, 40, '#dc2626') // obesidad 2 (rojo)
+ arc(40, 50, '#7f1d1d'); // obesidad 3 (rojo oscuro)
// Labels at zone centers
const zoneLabel = (v, txt) => {
const a = (-90 + ((v - min) / (max - min)) * 180) * Math.PI / 180;
const lx = cx + 102 * Math.cos(a - Math.PI/2);
const ly = cy + 102 * Math.sin(a - Math.PI/2);
return `${txt}`;
};
return `
`;
}
// ════════════════════════════════════════════════════════════
// ANIMATED COUNTER (smooth count-up effect for dopamine UI)
// ════════════════════════════════════════════════════════════
function animateCounter(el, target, opts = {}) {
if (!el) return;
const duration = opts.duration || 1200;
const decimals = opts.decimals != null ? opts.decimals : 0;
const prefix = opts.prefix || '';
const suffix = opts.suffix || '';
const startV = 0;
const startT = performance.now();
function tick(now) {
const elapsed = now - startT;
const progress = Math.min(elapsed / duration, 1);
// ease-out cubic
const eased = 1 - Math.pow(1 - progress, 3);
const v = startV + (target - startV) * eased;
el.textContent = prefix + v.toFixed(decimals) + suffix;
if (progress < 1) requestAnimationFrame(tick);
else el.textContent = prefix + target.toFixed(decimals) + suffix;
}
requestAnimationFrame(tick);
}
function animateAllCounters() {
document.querySelectorAll('[data-counter]').forEach(el => {
const target = parseFloat(el.dataset.counter);
if (isNaN(target)) return;
const decimals = parseInt(el.dataset.decimals || '0');
const prefix = el.dataset.prefix || '';
const suffix = el.dataset.suffix || '';
animateCounter(el, target, { decimals, prefix, suffix });
});
}
// Multiple body fat formulas
function multipleBfFormulas(M, sexo, edad, dwResult) {
const W = M.peso, T = M.talla;
const sum6 = M.triceps + M.subescapular + M.supraespinal + M.abdominal + M.pliegueMuslo + M.plieguePierna;
const sum8 = sum6 + M.biceps + M.crestaIliaca;
const m = sexo === 'masculino';
// Yuhasz (2 sites? actually 6-skinfold: tri+subscap+suprasp+abd+thigh+calf)
const yuhasz = m ? (sum6 * 0.097 + 3.64) : (sum6 * 0.143 + 4.56);
// Faulkner (4 skinfolds: triceps + subscap + suprailiac + abdominal)
const sum4F = M.triceps + M.subescapular + M.crestaIliaca + M.abdominal;
const faulkner = sum4F * 0.153 + 5.783;
// Carter (modified Yuhasz, 6 skinfolds with stretch correction)
const corr = 170.18 / T;
const carter = m
? (sum6 * corr) * 0.1051 + 2.585
: (sum6 * corr) * 0.1545 + 3.580;
// Jackson-Pollock 7-site (chest, abdomen, thigh, triceps, subscap, suprailiac, axilla approx)
// We don't have chest/axilla. Use 3-site instead:
// Men: chest, abdomen, thigh — we use abdominal + pliegueMuslo + supraespinal (proxy)
// Women: triceps, suprailiac, thigh
const sum3JP = m
? (M.abdominal + M.pliegueMuslo + M.supraespinal)
: (M.triceps + M.crestaIliaca + M.pliegueMuslo);
const dJP = m
? 1.10938 - 0.0008267*sum3JP + 0.0000016*sum3JP*sum3JP - 0.0002574*edad
: 1.0994921 - 0.0009929*sum3JP + 0.0000023*sum3JP*sum3JP - 0.0001392*edad;
const jacksonPollock = (4.95/dJP - 4.5) * 100;
// Slaughter (ages 8-18, triceps + calf)
const slaughter = edad >= 8 && edad <= 18
? (m ? 0.735*(M.triceps+M.plieguePierna)+1.0
: 0.610*(M.triceps+M.plieguePierna)+5.1)
: null;
// Rocha (Brazilian, kerr-tissue derived) — using kerr adipose
// Fallback: use durnin-womersley as canonical
const dw = dwResult ? dwResult.fatPct : 0;
return [
{ name: 'Durnin-Womersley (1974)', sites: '4 pliegues', val: dw, grams: (dw/100)*W, ref: true },
{ name: 'Yuhasz (1974)', sites: '6 pliegues', val: yuhasz, grams: (yuhasz/100)*W },
{ name: 'Faulkner (1968)', sites: '4 pliegues', val: faulkner, grams: (faulkner/100)*W },
{ name: 'Carter (1980)', sites: '6 pliegues', val: carter, grams: (carter/100)*W },
{ name: 'Jackson-Pollock (1978)', sites: '3 pliegues', val: jacksonPollock,grams: (jacksonPollock/100)*W },
slaughter != null ? { name: 'Slaughter (1988) [niños/adolescentes]', sites: 'tríceps+pierna', val: slaughter, grams: (slaughter/100)*W } : null
].filter(Boolean);
}
// Hydration analysis
function hydrationStatus(urineColor, density, weight) {
let level = 'unknown', light = 'amber', txt = 'Sin datos suficientes';
let recommendations = [];
let score = 0;
if (urineColor > 0) {
if (urineColor <= 3) { level='Bien hidratado'; light='green'; score+=2; }
else if (urineColor <= 5) { level='Hidratación leve'; light='amber'; score+=0; }
else { level='Deshidratación'; light='red'; score-=2; }
txt = `Color de orina ${urineColor}/8 (Armstrong) → ${level}`;
}
if (density > 0) {
let dLevel;
if (density < 1.020) { dLevel='Normal'; light = light==='red'?'red':'green'; score+=1; }
else if (density < 1.030) { dLevel='Deshidratación leve'; light = light==='red'?'red':'amber'; score-=1; }
else { dLevel='Deshidratación severa'; light='red'; score-=2; }
txt += `${urineColor>0?' · ':''}Densidad ${density.toFixed(3)} g/mL → ${dLevel}`;
}
// Recommendations
const baseMl = Math.round(weight * 35);
recommendations.push(`💧 Ingesta basal recomendada: ${baseMl} mL/día (35 mL/kg)`);
if (score < 0) recommendations.push(`⚠️ Aumentar ingesta a ${Math.round(weight*45)} mL/día las próximas 24-48h`);
if (urineColor >= 6 || density >= 1.030) recommendations.push(`🥤 Considerar bebida con electrolitos (Na 0.5-1.0 g/L)`);
if (urineColor === 0 && !density) {
recommendations = [`💧 Ingesta basal recomendada: ${baseMl} mL/día (35 mL/kg)`,
`📊 Para evaluar hidratación funcional: registra color de orina o densidad`];
light = 'info'; level = 'Sin medición'; txt = '—';
}
return { level, light, txt, recommendations, score };
}
// Athlete somatotype reference (Carter & Heath data)
const ATHLETE_REFS = [
// [endo, meso, ecto] elite means
{ sport: 'Fútbol', icon: '⚽', somat: [2.5, 5.0, 2.5] },
{ sport: 'Sprinter 100m', icon: '🏃', somat: [2.0, 5.0, 3.0] },
{ sport: 'Maratón', icon: '🏃♂️', somat: [1.5, 4.0, 3.5] },
{ sport: 'Boxeo pesado', icon: '🥊', somat: [3.0, 6.5, 1.5] },
{ sport: 'Físicoculturismo',icon:'💪', somat: [2.0, 7.5, 1.0] },
{ sport: 'Natación', icon: '🏊', somat: [2.0, 5.0, 2.5] },
{ sport: 'Tenis', icon: '🎾', somat: [2.5, 5.0, 2.5] },
{ sport: 'Básquetbol', icon: '🏀', somat: [2.5, 4.5, 3.0] },
{ sport: 'Rugby forward', icon: '🏉', somat: [3.5, 6.0, 1.5] },
{ sport: 'Voleibol', icon: '🏐', somat: [2.5, 4.5, 3.0] },
{ sport: 'Crossfit', icon: '🏋️', somat: [2.5, 6.5, 2.0] },
{ sport: 'Ciclismo', icon: '🚴', somat: [2.0, 4.5, 3.0] },
{ sport: 'Halterofilia', icon: '🏋️♂️', somat: [3.0, 7.0, 1.0] },
{ sport: 'Gimnasia', icon: '🤸', somat: [1.5, 5.5, 2.5] }
];
function athleteDistance(somatA, somatB) {
// Somatotype distance (SAD - Somatotype Attitudinal Distance)
return Math.sqrt(
Math.pow(somatA[0]-somatB[0], 2) +
Math.pow(somatA[1]-somatB[1], 2) +
Math.pow(somatA[2]-somatB[2], 2)
);
}
function findClosestAthletes(somato) {
const me = [somato.endo, somato.meso, somato.ecto];
return ATHLETE_REFS
.map(a => ({ ...a, distance: athleteDistance(me, a.somat) }))
.sort((a,b) => a.distance - b.distance);
}
// Goal projection
function goalProjection(M, sexo, currentBfPct, targetBfPct, tdee, targetCalories) {
const W = M.peso;
const ffm = W * (1 - currentBfPct/100);
// Target weight assuming FFM remains constant
const targetWeight = ffm / (1 - targetBfPct/100);
const deltaWeight = targetWeight - W;
// Caloric deficit/surplus
const dailyDelta = targetCalories - tdee;
// 1 kg fat ≈ 7700 kcal
const weeksToGoal = dailyDelta !== 0
? Math.abs(deltaWeight) / (Math.abs(dailyDelta) * 7 / 7700)
: 0;
const direction = deltaWeight < 0 ? 'loss' : 'gain';
// Sustainable rate check
const weeklyKg = (Math.abs(dailyDelta) * 7) / 7700;
let sustainability = 'optimal';
if (weeklyKg > 1.0) sustainability = 'aggressive';
else if (weeklyKg < 0.25) sustainability = 'slow';
return {
currentWeight: W, ffm, targetWeight, deltaWeight,
dailyDelta, weeksToGoal, direction,
weeklyKg, sustainability,
targetDate: weeksToGoal > 0 ? new Date(Date.now() + weeksToGoal*7*86400000) : null
};
}
// Risk alerts
function generateAlerts(M, sexo, edad, hi, dwPct, tissues, dist, somato) {
const alerts = [];
// BMI risks
if (hi.imc < 18.5) alerts.push({ level:'warning', icon:'⚠️', title:'IMC bajo', text:`IMC ${hi.imc.toFixed(1)} indica bajo peso. Evaluar ingesta calórica y descartar patologías.` });
if (hi.imc >= 30) alerts.push({ level:'critical', icon:'🚨', title:'Obesidad', text:`IMC ${hi.imc.toFixed(1)} indica obesidad ${hi.imc>=40?'grado 3':hi.imc>=35?'grado 2':'grado 1'}. Riesgo cardiometabólico aumentado.` });
// WHtR
if (hi.whtr >= 0.5) alerts.push({ level:'warning', icon:'📏', title:'Cintura/Talla elevada', text:`WHtR ${hi.whtr.toFixed(2)} ≥ 0.5: mayor riesgo metabólico. Recomendable reducir grasa abdominal.` });
// WHR
if (hi.whr >= hi.whrLimit) alerts.push({ level:'warning', icon:'📐', title:'Distribución androide', text:`WHR ${hi.whr.toFixed(2)} sobre el límite (${hi.whrLimit}). Acumulación abdominal.` });
// Fat % extremes
const m = sexo === 'masculino';
const minHealthyBf = m ? 6 : 14;
const obeseBf = m ? 25 : 32;
if (dwPct < minHealthyBf) alerts.push({ level:'critical', icon:'🚨', title:'% graso esencial', text:`% graso ${dwPct.toFixed(1)}% bajo el mínimo saludable (${minHealthyBf}%). Riesgo hormonal/inmune.` });
if (dwPct > obeseBf) alerts.push({ level:'critical', icon:'🚨', title:'% graso elevado', text:`% graso ${dwPct.toFixed(1)}% en rango de obesidad. Plan de reducción recomendado.` });
// Muscle mass low
const musclePct = (tissues.muscle / M.peso) * 100;
if (musclePct < 30) alerts.push({ level:'warning', icon:'💪', title:'Masa muscular baja', text:`${musclePct.toFixed(1)}% de masa muscular bajo el promedio. Considerar entrenamiento de fuerza.` });
// Age-specific
if (edad >= 65 && musclePct < 35) alerts.push({ level:'warning', icon:'👴', title:'Sarcopenia potencial', text:`Adulto mayor con masa muscular reducida. Evaluar fuerza y movilidad.` });
// Bone tissue
const bonePct = (tissues.bone / M.peso) * 100;
if (bonePct < 8) alerts.push({ level:'info', icon:'🦴', title:'Masa ósea baja', text:`${bonePct.toFixed(1)}% de masa ósea. Asegurar calcio, vitamina D y carga mecánica.` });
// Distribution
if (dist.fatTopPct > 40) alerts.push({ level:'info', icon:'📊', title:'Adiposidad superior', text:`${dist.fatTopPct.toFixed(1)}% de la grasa en zona superior.` });
if (dist.fatMidPct > 40) alerts.push({ level:'warning', icon:'📊', title:'Adiposidad central', text:`${dist.fatMidPct.toFixed(1)}% de la grasa en zona central — mayor riesgo metabólico.` });
// Somatotype
if (somato.endo > 6) alerts.push({ level:'info', icon:'🟠', title:'Endomorfismo dominante', text:`Componente endo ${somato.endo.toFixed(1)}. Alta tendencia adiposa.` });
// Positive
if (alerts.length === 0) alerts.push({ level:'success', icon:'✅', title:'Sin alertas', text:'Todos los indicadores antropométricos están dentro de rangos saludables.' });
return alerts;
}
// ════════════════════════════════════════════════════════════
// CONCLUSIONS + MEDICAL REFERRAL ENGINE
// ════════════════════════════════════════════════════════════
function generateConclusions(M, patient, sexo, edad, hi, dwPct, tissues, dist, somato, leeResult, bp) {
const conclusions = [];
const referrals = [];
const m = sexo === 'masculino';
// Build narrative conclusions
const nombre = patient.nombre || 'El/la paciente';
let dominantSoma = 'intermedio';
const maxComp = Math.max(somato.endo, somato.meso, somato.ecto);
if (maxComp === somato.endo) dominantSoma = 'endomórfico';
else if (maxComp === somato.meso) dominantSoma = 'mesomórfico';
else dominantSoma = 'ectomórfico';
conclusions.push(`${nombre} presenta un IMC de ${hi.imc.toFixed(1)} kg/m² (${hi.bmiCls.txt}), con un porcentaje de masa grasa estimado en ${dwPct.toFixed(1)}% (Durnin-Womersley). El perfil somatotípico es ${dominantSoma} (${somato.endo.toFixed(1)}–${somato.meso.toFixed(1)}–${somato.ecto.toFixed(1)}).`);
conclusions.push(`La composición corporal por fraccionamiento KERR muestra ${(tissues.muscle/M.peso*100).toFixed(1)}% de masa muscular, ${(tissues.adipose/M.peso*100).toFixed(1)}% de masa adiposa y ${(tissues.bone/M.peso*100).toFixed(1)}% de masa ósea.`);
if (leeResult) {
conclusions.push(`La masa muscular esquelética por ecuación de Lee et al. (2000) es de ${leeResult.smm.toFixed(1)} kg (índice SMI: ${leeResult.smi.toFixed(2)} kg/m² — ${leeResult.classification.label}).`);
}
// Distribution
if (dist.fatMidPct > 40) {
conclusions.push(`La distribución adiposa es predominantemente central (${dist.fatMidPct.toFixed(1)}%), lo cual está asociado con mayor riesgo cardiometabólico.`);
} else if (dist.fatTopPct > 40) {
conclusions.push(`La distribución adiposa es predominantemente superior (${dist.fatTopPct.toFixed(1)}%).`);
} else {
conclusions.push(`La distribución adiposa es proporcionada entre las regiones evaluadas.`);
}
// Health risks summary
const risks = [];
if (hi.whtr >= 0.5) risks.push('cintura/talla elevada');
if (hi.imc >= 30) risks.push('obesidad');
if (hi.imc < 18.5) risks.push('bajo peso');
if (bp && bp.pas >= 140) risks.push('hipertensión sistólica');
if (bp && bp.pad >= 90) risks.push('hipertensión diastólica');
if (bp && bp.glicemia >= 126) risks.push('hiperglicemia (criterio diagnóstico glicemia ayunas)');
if (bp && bp.hba1c >= 6.5) risks.push('HbA1c elevada');
if (bp && bp.colesterolT >= 240) risks.push('hipercolesterolemia');
if (bp && bp.trigliceridos >= 200) risks.push('hipertrigliceridemia');
if (risks.length) {
conclusions.push(`Marcadores de riesgo detectados: ${risks.join(', ')}.`);
} else {
conclusions.push(`No se detectaron marcadores de riesgo elevado en los parámetros evaluados.`);
}
// ─── REFERRALS ───
if (hi.imc >= 30 || dwPct > (m ? 25 : 32)) {
referrals.push({
to: '🥗 Nutricionista',
reason: 'Plan de intervención nutricional para reducción de masa grasa.',
urgency: 'recommended'
});
}
if (hi.imc < 18.5 || dwPct < (m ? 6 : 14)) {
referrals.push({
to: '🥗 Nutricionista + 👨⚕️ Médico',
reason: 'Bajo peso o % graso esencial bajo — evaluar etiología y riesgo nutricional/hormonal.',
urgency: 'priority'
});
}
if (bp && (bp.pas >= 140 || bp.pad >= 90)) {
referrals.push({
to: '👨⚕️ Médico (cardiología / medicina interna)',
reason: `Presión arterial elevada (${bp.pas}/${bp.pad} mmHg). Evaluar diagnóstico y manejo.`,
urgency: 'priority'
});
}
if (bp && (bp.glicemia >= 126 || bp.hba1c >= 6.5)) {
referrals.push({
to: '👨⚕️ Médico (endocrinología / diabetología)',
reason: 'Marcadores compatibles con disglicemia. Confirmar diagnóstico.',
urgency: 'priority'
});
}
if (bp && (bp.colesterolT >= 240 || bp.ldl >= 160 || bp.trigliceridos >= 200)) {
referrals.push({
to: '👨⚕️ Médico (cardiología) + 🥗 Nutricionista',
reason: 'Dislipidemia. Evaluar riesgo cardiovascular global.',
urgency: 'recommended'
});
}
if (leeResult && leeResult.classification.label.startsWith('Sarcopenia')) {
referrals.push({
to: '🏋️ Kinesiólogo / Preparador físico',
reason: 'Masa muscular reducida — programa de entrenamiento de fuerza supervisado.',
urgency: 'recommended'
});
}
if (patient.lesiones && patient.lesiones.trim()) {
referrals.push({
to: '🏥 Kinesiólogo / Médico deportólogo',
reason: 'Antecedentes de lesiones declarados — evaluar rehabilitación y reincorporación deportiva segura.',
urgency: 'recommended'
});
}
if (edad >= 65 && (tissues.muscle / M.peso * 100) < 35) {
referrals.push({
to: '👨⚕️ Médico geriatra + 🏋️ Kinesiólogo',
reason: 'Adulto mayor con masa muscular reducida — evaluar fuerza, marcha y prevención de caídas.',
urgency: 'priority'
});
}
return { conclusions, referrals };
}
function muscleAdiposeDistribution(M) {
const PI = Math.PI;
// Fat distribution from sum of 6 skinfolds
const top = M.triceps + M.subescapular;
const mid = M.supraespinal + M.abdominal;
const low = M.pliegueMuslo + M.plieguePierna;
const sum6 = top + mid + low;
const sum8 = sum6 + M.biceps + M.crestaIliaca;
// Corrected girths (Kerr / De Rose convention: π × skinfold/10, RELAXED arm)
const cArm = M.brazo - (PI * M.triceps / 10);
const cThigh = M.musloG - (PI * M.pliegueMuslo / 10);
const cCalf = M.pierna - (PI * M.plieguePierna/ 10);
const cTorax = M.torax - (PI * M.subescapular / 10);
const sumG = cArm + cThigh + cCalf;
return {
sum6, sum8,
top, mid, low,
fatTopPct: sum6 ? (top/sum6)*100 : 0,
fatMidPct: sum6 ? (mid/sum6)*100 : 0,
fatLowPct: sum6 ? (low/sum6)*100 : 0,
cArm, cThigh, cCalf, cTorax,
muscArmPct: sumG ? (cArm/sumG)*100 : 0,
muscThighPct: sumG ? (cThigh/sumG)*100 : 0,
muscCalfPct: sumG ? (cCalf/sumG)*100 : 0
};
}
// ════════════════════════════════════════════════════════════
// STORAGE / HISTORY
// ════════════════════════════════════════════════════════════
function loadHistory() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : [];
} catch { return []; }
}
function saveHistory(arr) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(arr));
refreshHistCount();
return true;
} catch (e) {
toast('No se pudo guardar (almacenamiento lleno)', 'error');
return false;
}
}
function refreshHistCount() {
document.getElementById('hist-count').textContent = loadHistory().length;
}
function guardarHistorial() {
if (!lastResult) { toast('Primero realiza un cálculo', 'warn'); return; }
const hist = loadHistory();
const record = {
id: Date.now(),
savedAt: new Date().toISOString(),
patient: lastResult.patient,
measurements: lastResult.measurements,
final: lastResult.final,
photo: lastResult.photo
};
hist.unshift(record);
if (saveHistory(hist)) toast('Evaluación guardada en historial', 'success');
}
function deleteRecord(id) {
if (!confirm('¿Eliminar esta evaluación del historial? Esta acción no se puede deshacer.')) return;
const hist = loadHistory().filter(r => r.id !== id);
selectedIds.delete(id);
saveHistory(hist);
renderHistoryList();
toast('Evaluación eliminada', 'success');
}
function loadRecord(id) {
const rec = loadHistory().find(r => r.id === id);
if (!rec) return;
// Fill identification
document.getElementById('nombre').value = rec.patient.nombre || '';
document.getElementById('documento').value = rec.patient.documento || '';
document.getElementById('telefono').value = rec.patient.telefono || '';
document.getElementById('evaluador').value = rec.patient.evaluador || '';
document.getElementById('fechaEval').value = rec.patient.fecha || todayIso();
document.getElementById('objetivo').value = rec.patient.objetivo || '';
document.getElementById('deporte').value = rec.patient.deporte || '';
document.getElementById('nivelActividad').value = rec.patient.nivelActividad || '';
document.getElementById('acreditacion').value = rec.patient.acreditacion || '';
document.getElementById('evalNum').value = rec.patient.evalNum || '';
document.getElementById('notas').value = rec.patient.notas || '';
if (rec.patient.actividadFisica) document.getElementById('actividadFisica').value = rec.patient.actividadFisica;
if (rec.patient.metaCalorica) document.getElementById('metaCalorica').value = rec.patient.metaCalorica;
if (rec.patient.objetivoGrasaPct) document.getElementById('objetivoGrasaPct').value = rec.patient.objetivoGrasaPct;
if (rec.patient.urineColor) setUrine(parseInt(rec.patient.urineColor));
if (rec.patient.urineDensity) document.getElementById('urineDensity').value = rec.patient.urineDensity;
// Sprint 2 fields
['fechaNacimiento','pais','etnia','etapaDeportiva','posicion','frecuenciaSem','horasSem','historialAnos','lesiones',
'pasMmhg','padMmhg','fcReposo','glicemia','hba1c','colesterolT','hdl','ldl','trigliceridos'].forEach(k => {
const el = document.getElementById(k);
if (el && rec.patient[k] != null && rec.patient[k] !== '') el.value = rec.patient[k];
});
if (rec.patient.consent) {
const chk = document.getElementById('consentChk');
if (chk) { chk.checked = true; onConsentCheck(); }
}
// Patient
setSexo(rec.patient.sexo || 'masculino');
document.getElementById('edad').value = rec.patient.edad ?? '';
document.getElementById('peso').value = rec.patient.peso ?? '';
document.getElementById('talla').value = rec.patient.talla ?? '';
document.getElementById('estatuSentado').value = rec.patient.sentado ?? '';
// Measurements
Object.keys(RANGES).forEach(k => {
const el = document.getElementById(k);
if (el) el.value = rec.measurements?.[k] ?? '';
});
// Photo
if (rec.photo) {
currentPhoto = rec.photo;
const img = document.getElementById('preview-img');
img.src = rec.photo;
img.style.display = 'block';
document.getElementById('foto-placeholder').style.display = 'none';
document.getElementById('foto-upload').classList.add('has-img');
} else {
clearPhoto();
}
closeHistory();
toast('Evaluación cargada – pulsa "Iniciar Análisis" para recalcular', 'success');
setTimeout(() => calcular(), 250);
}
// ── HISTORY MODAL ──
function openHistory() {
selectedIds.clear();
renderHistoryList();
document.getElementById('modal-history').classList.add('open');
document.body.style.overflow = 'hidden';
}
function closeHistory() {
document.getElementById('modal-history').classList.remove('open');
document.body.style.overflow = '';
}
function renderHistoryList() {
const container = document.getElementById('history-content');
const hist = loadHistory();
const q = (document.getElementById('hist-search').value || '').toLowerCase().trim();
const filtered = q
? hist.filter(r =>
(r.patient.nombre || '').toLowerCase().includes(q) ||
(r.patient.documento || '').toLowerCase().includes(q))
: hist;
if (filtered.length === 0) {
container.innerHTML = `
${hist.length === 0 ? '📭' : '🔍'}
${hist.length === 0 ? 'Aún no has guardado evaluaciones' : 'Sin resultados para tu búsqueda'}
`;
updateCompareBtn();
return;
}
// Evolution chart: if filtering by name and >=2 records of same patient
renderEvolutionChart(filtered, q);
let html = '
${isakFoot(2, totalPages, p.evaluador)}
`;
}
// SVG body silhouette with labels
function isakBodySvg(side) {
// Simplified human silhouette
return `
`;
}
// PAGE 3: Distribution + Body comp indices
function isakPage3(p, M, totalPages, dist, bci) {
return `
${isakLogo()}
Fat mass
Muscle tissue
Muscular adipose distributionk
Fat mass
${isakBodySvg('fat')}
Top: ${dist.fatTopPct.toFixed(2)}%
Central: ${dist.fatMidPct.toFixed(2)}%
Lower: ${dist.fatLowPct.toFixed(2)}%
Arm: ${dist.muscArmPct.toFixed(2)}%
Thigh: ${dist.muscThighPct.toFixed(2)}%
Calf: ${dist.muscCalfPct.toFixed(2)}%
Muscle tissue
${isakBodySvg('muscle')}
Body composition indicesk
Adipose muscle index${bci.ami.toFixed(2)}
Classification: ${bci.amiCls}
Expresses how many kg. of adipose tissue has to be carried by each kg. of muscle tissue. The lower this number, the more efficient the displacement.
Muscle Bone index${bci.mbi.toFixed(2)}
Classification: ${bci.mbiCls}
Expresses how many kg. of muscle tissue are in the body relative to the bone tissue.
${currentLang==='en'
? 'The patient just opens this link on their phone to see their results in a beautiful read-only view. They can install it as an app from their browser.'
: 'El paciente solo abre este enlace en su teléfono para ver sus resultados en una vista bonita de solo lectura. Puede instalarlo como app desde su navegador.'}
📝 ${currentLang==='en'?'Notes from your evaluator':'Notas de tu evaluador'}
${escapeHtml(p.no)}
` : ''}
${t('pv.thanks')} ANTROPY · by SDEPORTIVA
`;
// Show install button when prompt is available
setTimeout(() => {
if (deferredInstallPrompt) {
const btn = document.getElementById('pv-install-btn');
if (btn) btn.hidden = false;
}
}, 800);
return true;
}
function toggleLangPV() {
setLang(currentLang === 'es' ? 'en' : 'es');
// Re-render patient view
document.getElementById('patient-view-host').innerHTML = '';
document.body.classList.remove('patient-mode');
tryRenderPatientView();
}
async function pwaInstall() {
if (!deferredInstallPrompt) {
toast(currentLang==='en' ? 'Use your browser menu: Add to Home Screen' : 'Usa el menú del navegador: Agregar a inicio', 'info', 4000);
return;
}
deferredInstallPrompt.prompt();
const choice = await deferredInstallPrompt.userChoice;
if (choice.outcome === 'accepted') toast(currentLang==='en' ? 'App installed!' : '¡App instalada!', 'success');
deferredInstallPrompt = null;
document.getElementById('pv-install-btn')?.setAttribute('hidden', '');
}
// ════════════════════════════════════════════════════════════
// DEMO DATA — Lucas Nervi (verification against ISAKMetry)
// ════════════════════════════════════════════════════════════
function loadDemoLucas() {
const D = {
nombre:'Lucas Nervi', documento:'', telefono:'', evaluador:'FELIPE MUÑOZ ZAMBRANO',
fechaEval:'2026-03-06', objetivo:'Rendimiento deportivo',
deporte:'', nivelActividad:'Competitivo',
acreditacion:'ISAK Level 2', evalNum:'2/2',
edad:24, peso:120.8, talla:186.5, estatuSentado:96.5,
triceps:17.0, subescapular:23.0, supraespinal:13.0, abdominal:22.0,
pliegueMuslo:23.0, plieguePierna:12.0, biceps:5.0, crestaIliaca:40.0,
cabeza:58.8, brazo:38.2, brazoFlex:41.0, antebrazo:33.2,
torax:119.0, cintura:103.2, cadera:125.2, musloG:71.2, pierna:44.2,
biacromial:43.9, biiliocrestal:34.2, humero:7.8, femur:10.8,
toraxAP:23.7, toraxTrans:35.1
};
Object.entries(D).forEach(([k,v]) => {
const el = document.getElementById(k);
if (el) el.value = v;
});
setSexo('masculino');
// Clear validation flags
document.querySelectorAll('.field.warn,.field.error').forEach(f => {
f.classList.remove('warn','error');
const m = f.querySelector('.field-msg');
if (m) m.textContent = '';
});
toast(t('t.demoLoaded'), 'success', 3000);
}
// ════════════════════════════════════════════════════════════
// GLOBAL UI EVENTS
// ════════════════════════════════════════════════════════════
// Close modals on Escape / overlay click
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
closeHistory();
closeComparison();
}
});
['modal-history','modal-compare','modal-share'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('click', e => { if (e.target === el) el.classList.remove('open'); });
});
// ════════════════════════════════════════════════════════════
// SPRINT 3 — SETTINGS, BACKUP, TEMPLATES, ROLES, AUDIT, BIA, XLSX
// ════════════════════════════════════════════════════════════
const SETTINGS_KEY = 'kerr5c_settings_v1';
const ROLE_KEY = 'kerr5c_role_v1';
const TEMPLATE_KEY = 'kerr5c_template_v1';
const AUDIT_KEY = 'kerr5c_audit_v1';
// ── Role system ──
const ROLES = {
admin: { name: 'Administrador', desc: 'Acceso completo: medir, guardar, exportar, configurar, eliminar', ico: '👑' },
evaluator:{ name: 'Evaluador', desc: 'Puede medir y guardar, ver historial. No accede a configuración.', ico: '🥼' },
readonly: { name: 'Solo lectura', desc: 'Ver historial e informes. No puede modificar nada.', ico: '👁️' }
};
let currentRole = 'admin';
function setRole(r) {
if (!ROLES[r]) r = 'admin';
currentRole = r;
document.body.classList.remove('role-admin','role-evaluator','role-readonly');
document.body.classList.add('role-' + r);
try { localStorage.setItem(ROLE_KEY, r); } catch {}
toast(`Rol cambiado: ${ROLES[r].ico} ${ROLES[r].name}`, 'success', 2500);
}
// ── Report templates ──
const TEMPLATES = {
full: { name: 'Completo', ico: '📑', desc: 'Todas las secciones (default)',
sections: ['summary','soma','ctrl','chart','goal','energy','hyd','bf','athlete','lee','models','health','alerts','conclusions','referrals'] },
athlete: { name: 'Atleta', ico: '🏆', desc: 'Composición, somatotipo, atleta ref, macros',
sections: ['summary','soma','ctrl','chart','goal','energy','bf','athlete','lee','alerts','conclusions'] },
clinical: { name: 'Clínico', ico: '🏥', desc: 'Mediciones, salud, alertas, derivación',
sections: ['summary','soma','ctrl','chart','health','alerts','conclusions','referrals'] },
geriatric: { name: 'Geriátrico', ico: '👴', desc: 'Sarcopenia (Lee SMI), riesgos',
sections: ['summary','ctrl','lee','health','alerts','conclusions','referrals'] },
pediatric: { name: 'Pediátrico', ico: '🧒', desc: 'Niños/adolescentes con Slaughter',
sections: ['summary','soma','ctrl','chart','bf','alerts','conclusions'] },
esthetic: { name: 'Estética', ico: '✨', desc: 'Composición + objetivo + macros',
sections: ['summary','chart','goal','energy','bf','conclusions'] }
};
let currentTemplate = 'full';
function setTemplate(t) {
if (!TEMPLATES[t]) t = 'full';
currentTemplate = t;
try { localStorage.setItem(TEMPLATE_KEY, t); } catch {}
if (lastResult) renderResults(lastResult);
}
// ── Audit log ──
function loadAudit() {
try { return JSON.parse(localStorage.getItem(AUDIT_KEY) || '[]'); } catch { return []; }
}
function appendAudit(action, details) {
const audit = loadAudit();
audit.unshift({
ts: new Date().toISOString(),
role: currentRole,
who: _v('evaluador') || ROLES[currentRole].name,
action,
details
});
// Cap at 500 entries
if (audit.length > 500) audit.length = 500;
try { localStorage.setItem(AUDIT_KEY, JSON.stringify(audit)); } catch {}
}
// ── Settings modal ──
let currentSettingsTab = 'backup';
function openSettings() {
document.getElementById('modal-settings').classList.add('open');
document.body.style.overflow = 'hidden';
setSettingsTab(currentSettingsTab);
}
function closeSettings() {
document.getElementById('modal-settings').classList.remove('open');
document.body.style.overflow = '';
}
function setSettingsTab(tab) {
currentSettingsTab = tab;
document.querySelectorAll('.settings-tab').forEach(el => {
el.classList.toggle('active', el.dataset.tab === tab);
});
const c = document.getElementById('settings-tab-content');
if (tab === 'backup') c.innerHTML = renderBackupTab();
else if (tab === 'templates') c.innerHTML = renderTemplatesTab();
else if (tab === 'roles') c.innerHTML = renderRolesTab();
else if (tab === 'audit') c.innerHTML = renderAuditTab();
}
// ─── BACKUP / RESTORE ───
function renderBackupTab() {
const hist = loadHistory();
const audit = loadAudit();
return `
📥 Exportar respaldo completo (JSON)
Descarga un único archivo JSON con todo tu historial (${hist.length} evaluaciones), logo de marca, auditoría (${audit.length} eventos) y preferencias. Úsalo para hacer respaldo, mover a otro dispositivo o compartir con un colega.
📤 Restaurar desde respaldo
Carga un archivo de respaldo previamente exportado. ⚠️ Reemplaza todo tu historial actual — exporta primero si tienes datos importantes.
⚠️ Borrado seguro
Elimina TODOS los datos locales: historial, logo, auditoría, preferencias. Acción irreversible.
`;
}
function exportBackup() {
const data = {
appVersion: 'ANTROPY v3 (Sprint 3)',
exportedAt: new Date().toISOString(),
history: loadHistory(),
audit: loadAudit(),
logo: currentLogo,
lang: currentLang,
role: currentRole,
template: currentTemplate
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type:'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `ANTROPY_backup_${todayIso()}.json`;
document.body.appendChild(a); a.click();
setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 200);
appendAudit('backup_export', `${data.history.length} eval, ${data.audit.length} eventos`);
toast('Respaldo descargado', 'success');
}
function importBackup(event) {
const file = event.target.files?.[0];
if (!file) return;
if (!confirm('Esta acción REEMPLAZA tu historial actual. ¿Continuar?')) return;
const r = new FileReader();
r.onload = ev => {
try {
const data = JSON.parse(ev.target.result);
if (!data || !Array.isArray(data.history)) throw new Error('Formato inválido');
localStorage.setItem(STORAGE_KEY, JSON.stringify(data.history));
if (Array.isArray(data.audit)) localStorage.setItem(AUDIT_KEY, JSON.stringify(data.audit));
if (data.logo) localStorage.setItem(LOGO_KEY, data.logo);
if (data.lang) localStorage.setItem(LANG_KEY, data.lang);
if (data.role) localStorage.setItem(ROLE_KEY, data.role);
if (data.template) localStorage.setItem(TEMPLATE_KEY, data.template);
refreshHistCount();
appendAudit('backup_import', `${data.history.length} eval restauradas`);
toast('Respaldo restaurado · recarga la página para aplicar todo', 'success', 4500);
setSettingsTab('backup');
} catch (e) {
toast('Error al leer el respaldo: ' + e.message, 'error');
}
};
r.readAsText(file);
event.target.value = '';
}
function wipeAllData() {
if (!confirm('⚠️ Esto borrará TODO: historial, logo, auditoría, preferencias. ¿Estás seguro?')) return;
if (!confirm('Última confirmación. ¿Borrar TODOS los datos locales?')) return;
[STORAGE_KEY, AUDIT_KEY, LOGO_KEY, LANG_KEY, THEME_KEY, ROLE_KEY, TEMPLATE_KEY, PROFILE_KEY, QC_KEY, SETTINGS_KEY].forEach(k => {
try { localStorage.removeItem(k); } catch {}
});
toast('Todos los datos borrados. Recargando…', 'warn', 1500);
setTimeout(() => location.reload(), 1500);
}
// ─── TEMPLATES ───
function renderTemplatesTab() {
return `
📋 Plantilla de informe activa
Define qué secciones aparecen en los resultados y el PDF. Se aplica inmediatamente. Útil para adaptar el informe al tipo de paciente.
Cada paper se presenta en 3 formatos para distintos estilos de aprendizaje: documento completo, podcast narrado para escuchar mientras entrenas, e infografía visual para captar la idea en segundos.
Abrir biblioteca →
`;
}
function renderDashboardCharts(hist) {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
const txtColor = isDark ? '#e8eef5' : '#1E2D3D';
// Year chart
const yc = document.getElementById('dash-year');
if (yc && window._dashByYear) {
const yKeys = Object.keys(window._dashByYear).sort();
new Chart(yc, {
type: 'bar',
data: { labels: yKeys, datasets: [{
label: 'Evaluaciones', data: yKeys.map(k => window._dashByYear[k]),
backgroundColor: yKeys.map((_,i) => `hsl(${(i*40+200)%360}, 70%, 55%)`),
borderRadius: 8
}]},
options: { responsive:true, maintainAspectRatio:false,
plugins:{ legend:{ display:false } },
scales:{ x:{ ticks:{color:txtColor} }, y:{ ticks:{color:txtColor}, beginAtZero:true } }
}
});
}
// Age chart
const ac = document.getElementById('dash-age');
if (ac && window._dashAgeGroups) {
const labels = Object.keys(window._dashAgeGroups);
new Chart(ac, {
type: 'bar',
data: { labels, datasets: [{
label: 'Pacientes', data: labels.map(k => window._dashAgeGroups[k]),
backgroundColor: 'linear-gradient(45deg, #00AEEF, #7c3aed)',
backgroundColor: ['#00AEEF','#0090CC','#7c3aed','#a855f7','#ec4899','#f97316','#16a34a'],
borderRadius: 8
}]},
options: { responsive:true, maintainAspectRatio:false,
plugins:{ legend:{ display:false } },
scales:{ x:{ ticks:{color:txtColor} }, y:{ ticks:{color:txtColor}, beginAtZero:true } }
}
});
}
}
// Add modal-dashboard to overlay click handlers
const _dashOverlayEl = document.getElementById('modal-dashboard');
if (_dashOverlayEl) _dashOverlayEl.addEventListener('click', e => { if (e.target === _dashOverlayEl) _dashOverlayEl.classList.remove('open'); });
// ════════════════════════════════════════════════════════════
// PAPERS BIBLIOTECA — Paper + Podcast + Infografía
// ════════════════════════════════════════════════════════════
const PAPERS_KEY = 'kerr5c_papers_custom_v1';
// Seed papers (clásicos de antropometría y cineantropometría)
const SEED_PAPERS = [
{
id: 'kerr-1988',
title: 'An Anthropometric Method for Fractionation of Skin, Adipose, Bone, Muscle and Residual Tissue Masses',
authors: 'Kerr DA',
year: 1988,
journal: 'PhD Thesis · Simon Fraser University',
tags: ['KERR','5 componentes','composición corporal','tesis'],
duration: '14:22',
grad: ['#0C3547','#0090CC'],
ico: '🔬',
abstract: 'Tesis doctoral original que estableció el método KERR de fraccionamiento en 5 componentes. Validado contra disección de cadáveres del estudio belga de Bruselas. Base científica del motor de cálculo de ANTROPY.',
pdfUrl: 'https://summit.sfu.ca/item/4655',
audioUrl: '',
infographicUrl: ''
},
{
id: 'carter-heath-1990',
title: 'Somatotyping — Development and Applications',
authors: 'Carter JEL, Heath BH',
year: 1990,
journal: 'Cambridge University Press',
tags: ['somatotipo','Heath-Carter','antropometría'],
duration: '12:08',
grad: ['#7c3aed','#a855f7'],
ico: '📐',
abstract: 'Libro definitorio sobre somatotipo Heath-Carter. Define las 3 componentes (endomorfia/mesomorfia/ectomorfia) y su aplicación en ciencias del deporte. Referencia obligada para clasificación corporal.',
pdfUrl: 'https://www.scribd.com/doc/110204321/Somatotyping-Carter-Heath',
audioUrl: '',
infographicUrl: ''
},
{
id: 'lee-2000',
title: 'Total-body skeletal muscle mass: development and cross-validation of anthropometric prediction models',
authors: 'Lee RC, Wang Z, Heo M, Ross R, Janssen I, Heymsfield SB',
year: 2000,
journal: 'Am J Clin Nutr · 72:796-803',
tags: ['masa muscular','SMM','antropometría','validación'],
duration: '8:45',
grad: ['#16a34a','#15803d'],
ico: '💪',
abstract: 'Estudio que desarrolla y valida ecuaciones antropométricas para estimar masa muscular esquelética total, validadas contra resonancia magnética. Incluye ajuste por etnia. Implementada en ANTROPY.',
pdfUrl: 'https://academic.oup.com/ajcn/article/72/3/796/4729537',
audioUrl: '',
infographicUrl: ''
},
{
id: 'durnin-womersley-1974',
title: 'Body fat assessed from total body density and its estimation from skinfold thickness',
authors: 'Durnin JVGA, Womersley J',
year: 1974,
journal: 'Br J Nutr · 32(1):77-97',
tags: ['% graso','pliegues','densidad','clásico'],
duration: '10:30',
grad: ['#f97316','#c2410c'],
ico: '🔥',
abstract: 'Estudio clásico que estableció las ecuaciones para estimar densidad corporal y % graso a partir de 4 pliegues. Validado contra pesaje hidrostático. Una de las fórmulas más usadas globalmente.',
pdfUrl: 'https://www.cambridge.org/core/journals/british-journal-of-nutrition/article/body-fat-assessed-from-total-body-density-and-its-estimation-from-skinfold-thickness/E1B0FED55B3D04F9028AB94D6FFE8E5D',
audioUrl: '',
infographicUrl: ''
},
{
id: 'norton-olds-1996',
title: 'Anthropometrica — A textbook of body measurement for sports and health courses',
authors: 'Norton K, Olds T (Eds.)',
year: 1996,
journal: 'UNSW Press',
tags: ['ISAK','metodología','libro'],
duration: '15:20',
grad: ['#ec4899','#be185d'],
ico: '📖',
abstract: 'Libro de texto australiano fundamental para el aprendizaje de antropometría aplicada a deporte y salud. Cubre técnicas, protocolos ISAK, análisis y aplicaciones. Lectura obligada en cursos de cineantropometría.',
pdfUrl: 'https://www.researchgate.net/publication/235796408_Anthropometrica',
audioUrl: '',
infographicUrl: ''
},
{
id: 'janssen-2002',
title: 'Skeletal muscle cutpoints associated with elevated physical disability risk in older men and women',
authors: 'Janssen I, Baumgartner RN, Ross R, Rosenberg IH, Roubenoff R',
year: 2002,
journal: 'Am J Epidemiol · 159(4):413-21',
tags: ['sarcopenia','SMI','riesgo','adulto mayor'],
duration: '7:50',
grad: ['#dc2626','#991b1b'],
ico: '👴',
abstract: 'Estudio que estableció los puntos de corte del Skeletal Muscle Mass Index (SMI) para clasificar sarcopenia y predecir riesgo de discapacidad física en adultos mayores. Implementado en ANTROPY (clasificación de Lee SMM).',
pdfUrl: 'https://academic.oup.com/aje/article/159/4/413/153090',
audioUrl: '',
infographicUrl: ''
},
{
id: 'isak-2019',
title: 'International Standards for Anthropometric Assessment (ISAK)',
authors: 'Stewart A, Marfell-Jones M, Olds T, de Ridder H',
year: 2019,
journal: 'ISAK · Murcia, Spain',
tags: ['ISAK','estándares','protocolo','TEM'],
duration: '18:10',
grad: ['#0090CC','#0050A0'],
ico: '📏',
abstract: 'Manual oficial de ISAK con los estándares internacionales de evaluación antropométrica. Define puntos anatómicos, técnicas, instrumentos, control de calidad TEM y los perfiles restringido (15 sitios) y completo (38 sitios). Base normativa de ANTROPY.',
pdfUrl: 'https://www.isak.global/Education/AccreditationManual',
audioUrl: '',
infographicUrl: ''
},
{
id: 'ross-wilson-1974',
title: 'A stratagem for proportional growth assessment — The Phantom',
authors: 'Ross WD, Wilson NC',
year: 1974,
journal: 'Acta Paediatrica Belgica · 28(suppl):169-182',
tags: ['Phantom','Z-score','proporcionalidad'],
duration: '11:00',
grad: ['#14b8a6','#0f766e'],
ico: '👻',
abstract: 'Define el modelo Phantom de Ross & Wilson: un humano de referencia con valores P (media) y S (desviación estándar) para 30+ mediciones, que permite calcular Z-scores ajustados por estatura. Base de los Z-scores antropométricos en ANTROPY.',
pdfUrl: 'https://www.researchgate.net/publication/247524817_A_strategem_for_proportional_growth_assessment',
audioUrl: '',
infographicUrl: ''
}
];
let currentPapersFilter = 'all';
let currentPaperOpenId = null;
let currentPaperTab = 'pdf';
function loadCustomPapers() {
try { return JSON.parse(localStorage.getItem(PAPERS_KEY) || '[]'); } catch { return []; }
}
function saveCustomPapers(arr) {
try { localStorage.setItem(PAPERS_KEY, JSON.stringify(arr)); } catch {}
}
function allPapers() {
return [...SEED_PAPERS, ...loadCustomPapers()];
}
function openPapers() {
currentPaperOpenId = null;
renderPapersList();
document.getElementById('modal-papers').classList.add('open');
document.body.style.overflow = 'hidden';
appendAudit('papers_open', 'Biblioteca abierta');
}
function closePapers() {
document.getElementById('modal-papers').classList.remove('open');
document.body.style.overflow = '';
}
function renderPapersList() {
const content = document.getElementById('papers-content');
const papers = allPapers();
// Collect unique tags
const allTags = [...new Set(papers.flatMap(p => p.tags))];
const filtered = currentPapersFilter === 'all'
? papers
: papers.filter(p => p.tags.includes(currentPapersFilter));
content.innerHTML = `
El podcast de este paper aún no está disponible. Como administrador, puedes editar el paper y agregar la URL del audio (cualquier servicio: SoundCloud, Spotify embed, MP3 directo).
`}
📌 Notas del podcast:
${escapeHtml(paper.abstract)}