From 9bc7035d62c129e18c228283f2859dbc67fe8065 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Fri, 19 Sep 2025 14:29:03 -0400 Subject: [PATCH] Get ui logic into working state --- src/lib/data/standards.json | 84 ++++++------ src/lib/services/calculator/main.ts | 98 ++++++++++++-- src/lib/test/test-calculator.ts | 11 +- src/routes/+page.svelte | 202 ++++++++++++++-------------- 4 files changed, 240 insertions(+), 155 deletions(-) diff --git a/src/lib/data/standards.json b/src/lib/data/standards.json index 97a6157..6120a0d 100644 --- a/src/lib/data/standards.json +++ b/src/lib/data/standards.json @@ -4751,7 +4751,7 @@ { "metrics": { "age": 10, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4765,7 +4765,7 @@ { "metrics": { "age": 15, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4779,7 +4779,7 @@ { "metrics": { "age": 20, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4793,7 +4793,7 @@ { "metrics": { "age": 25, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4807,7 +4807,7 @@ { "metrics": { "age": 30, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4821,7 +4821,7 @@ { "metrics": { "age": 35, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4835,7 +4835,7 @@ { "metrics": { "age": 40, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4849,7 +4849,7 @@ { "metrics": { "age": 45, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4863,7 +4863,7 @@ { "metrics": { "age": 50, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4877,7 +4877,7 @@ { "metrics": { "age": 55, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4891,7 +4891,7 @@ { "metrics": { "age": 60, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4905,7 +4905,7 @@ { "metrics": { "age": 65, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4919,7 +4919,7 @@ { "metrics": { "age": 70, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4933,7 +4933,7 @@ { "metrics": { "age": 75, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4947,7 +4947,7 @@ { "metrics": { "age": 80, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4961,7 +4961,7 @@ { "metrics": { "age": 85, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4975,7 +4975,7 @@ { "metrics": { "age": 90, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -4989,7 +4989,7 @@ { "metrics": { "age": 10, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5003,7 +5003,7 @@ { "metrics": { "age": 15, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5017,7 +5017,7 @@ { "metrics": { "age": 20, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5031,7 +5031,7 @@ { "metrics": { "age": 25, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5045,7 +5045,7 @@ { "metrics": { "age": 30, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5059,7 +5059,7 @@ { "metrics": { "age": 35, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5073,7 +5073,7 @@ { "metrics": { "age": 40, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5087,7 +5087,7 @@ { "metrics": { "age": 45, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5101,7 +5101,7 @@ { "metrics": { "age": 50, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5115,7 +5115,7 @@ { "metrics": { "age": 55, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5129,7 +5129,7 @@ { "metrics": { "age": 60, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5143,7 +5143,7 @@ { "metrics": { "age": 65, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5157,7 +5157,7 @@ { "metrics": { "age": 70, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5171,7 +5171,7 @@ { "metrics": { "age": 75, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5185,7 +5185,7 @@ { "metrics": { "age": 80, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5199,7 +5199,7 @@ { "metrics": { "age": 85, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5213,7 +5213,7 @@ { "metrics": { "age": 90, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5230,6 +5230,12 @@ "metadata": { "attribute": "Power", "generators": [ + { + "metric": "age", + "spread": 4, + "step": 10, + "ratio": "inverse" + }, { "metric": "weight", "spread": 3, @@ -5243,8 +5249,8 @@ "standards": [ { "metrics": { - "age": 18, - "weight": -1, + "age": 20, + "weight": 0, "gender": "Male" }, "levels": { @@ -5258,7 +5264,7 @@ { "metrics": { "age": 18, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { @@ -5289,7 +5295,7 @@ { "metrics": { "age": 18, - "weight": -1, + "weight": 0, "gender": "Male" }, "levels": { @@ -5303,7 +5309,7 @@ { "metrics": { "age": 18, - "weight": -1, + "weight": 0, "gender": "Female" }, "levels": { diff --git a/src/lib/services/calculator/main.ts b/src/lib/services/calculator/main.ts index 685b0ed..3b2f359 100644 --- a/src/lib/services/calculator/main.ts +++ b/src/lib/services/calculator/main.ts @@ -1,4 +1,4 @@ -import { Activity, Attribute, Gender, getAvgWeight, lbToKg, type ActivityPerformance, type Player, type StandardUnit } from "./util"; +import { Activity, Attribute, Gender, getAvgWeight, kgToLb, lbToKg, type ActivityPerformance, type Player, type StandardUnit } from "./util"; // SOURCES // Squat, Bench, Dead Lift: @@ -182,7 +182,7 @@ export class LevelCalculator { } } -type StandardsConfig = { +export type StandardsConfig = { maxLevel: number } @@ -210,7 +210,55 @@ export class Standards { const ageGenerators = allGenerators.filter(g => g.metric === "age"); for (const ageGenerator of ageGenerators) { - // TODO + for (const gender of Object.values(Gender)) { + const referenceAge = 50; + + const skewRatio = 0; // 0 = normal, 1 = max skew + const minAge = referenceAge - (ageGenerator.step * ageGenerator.spread * (1 - skewRatio)); + const maxAge = referenceAge + (ageGenerator.step * ageGenerator.spread * (1 + skewRatio)); + + const referenceStandard = this + .byActivity(activity) + .byMetrics({ weight: getAvgWeight(gender, referenceAge), gender, age: referenceAge }) + .getOneInterpolated(); + + let currAge = minAge; + while (currAge <= maxAge) { + const newStandard: Standard = { + metrics: { + weight: getAvgWeight(gender, currAge), + age: currAge, + gender + }, + levels: {} + }; + + // apply allometric scaling to generate levels + for (const lvl in referenceStandard.levels) { + if (!referenceStandard.levels[lvl]) continue; + + const scalingExponent = .1; + const coefficient = ageGenerator.ratio === "inverse" ? + referenceAge / currAge : + currAge / referenceAge; + newStandard.levels[lvl] = referenceStandard.levels[lvl] * (coefficient ** scalingExponent); + } + + // prefer real data over generated data + const overlappingStandard = this + .byActivity(activity) + .byMetrics(newStandard.metrics) + .getOne(); + if (!overlappingStandard) + this.activityStandards[activity].standards.push(newStandard); + + currAge += ageGenerator.step; + } + } + + // now that we generated metric data, remove the old data + this.activityStandards[activity].standards = this.activityStandards[activity].standards + .filter(s => s.metrics.weight) } const weightGenerators = allGenerators.filter(g => g.metric === "weight"); @@ -247,6 +295,7 @@ export class Standards { const coefficient = weightGenerator.ratio === "inverse" ? referenceWeight / currWeight : currWeight / referenceWeight; + newStandard.levels[lvl] = referenceStandard.levels[lvl] * (coefficient ** scalingExponent); } @@ -265,9 +314,19 @@ export class Standards { // now that we generated metric data, remove the old data this.activityStandards[activity].standards = this.activityStandards[activity].standards - .filter(s => s.metrics.weight !== -1); + .filter(s => s.metrics.weight); } + this.activityStandards[activity].standards = this.activityStandards[activity].standards + .sort((a, b) => { + const genderOrder: Record = { Male: 0, Female: 1 }; + const gA = genderOrder[b.metrics.gender] ?? 99; + const gB = genderOrder[b.metrics.gender] ?? 99; + if (gA !== gB) return gA - gB; + if (a.metrics.age !== b.metrics.age) + return a.metrics.age - b.metrics.age; + return a.metrics.weight - b.metrics.weight; + }); } } @@ -324,6 +383,7 @@ export class Standards { }; const interpolateByAgeAndWeight = (metrics: Metrics): Standard => { + console.log(metrics); const interpolateByWeight = (targetAge: number): Levels => { const { lower: lowerAgeWeightStandard, upper: upperAgeWeightStandard } = this .byActivity(activity) @@ -367,10 +427,15 @@ export class Standards { } }; - const getAllInterpolated = (): Standard[] => { + const getAllInterpolated = (opt?: { normalizeForLb?: boolean }): Standard[] => { const metrics: Metrics = state as Metrics; - const interpolatedStandards: Standard[] = this.weightsFor(activity, metrics) + let weights = this.weightsFor(activity, metrics.gender, metrics.age); + if (opt?.normalizeForLb) { + weights = weights.map(w => lbToKg(Math.round(kgToLb(w)))); + } + + const interpolatedStandards: Standard[] = weights .map(weight => interpolateByAgeAndWeight({ ...metrics, weight })); return interpolatedStandards; @@ -420,17 +485,26 @@ export class Standards { return activities; } - public weightsFor(activity: Activity, metrics: Metrics) { - const { lower } = this + // TODO: turn these into queries? + public weightsFor(activity: Activity, gender: Gender, age: number) { + const { lower, upper } = this .byActivity(activity) - .byGender(metrics.gender) - .getNearest("age", metrics.age); + .byGender(gender) + .getNearest("age", age); + + const lowerDiff = Math.abs(lower.metrics.age - age); + const upperDiff = Math.abs(upper.metrics.age - age); + const closestAge = lowerDiff < upperDiff ? + lower.metrics.age : + upper.metrics.age; const weights = this .byActivity(activity) - .byGender(metrics.gender) - .byAge(lower.metrics.age) + .byGender(gender) + .byAge(closestAge) .getAll().map((s) => s.metrics.weight); + + console.log({ age, closestAge, weights }); return weights; } diff --git a/src/lib/test/test-calculator.ts b/src/lib/test/test-calculator.ts index 6249ec0..eb44531 100644 --- a/src/lib/test/test-calculator.ts +++ b/src/lib/test/test-calculator.ts @@ -1,4 +1,6 @@ -import { Activity, feetToCm, Gender, getAvgWeight, inchesToCm, kgToLb, lbToKg, minToMs, secToMs, type ActivityPerformance, type Player } from "$lib/services/calculator/util"; +import { Standards, type ActivityStandards } from "$lib/services/calculator/main"; +import { Activity, ftToCm, Gender, getAvgWeight, inToCm, kgToLb, lbToKg, minToMs, secToMs, type ActivityPerformance, type Player } from "$lib/services/calculator/util"; +import allStandardsRaw from "$lib/data/standards.json" assert { type: "json" }; const player: Player = { metrics: { @@ -25,7 +27,7 @@ const computedPerformances: ActivityPerformance[] = [ // POWER { activity: Activity.BroadJump, - performance: feetToCm(105) + inchesToCm(5), + performance: ftToCm(105) + inToCm(5), }, // ENDURANCE { @@ -39,10 +41,7 @@ const computedPerformances: ActivityPerformance[] = [ }, ]; -for (let age = 15; age < 80; ++age) { - const avgWeightKG = getAvgWeight(Gender.Female, age); - console.log(`${age}: ${kgToLb(avgWeightKG).toFixed(1)}`); -} +const standards = new Standards(allStandardsRaw as ActivityStandards, { maxLevel: 10 }); // const newStandards: Standards = { // "Back Squat": [], diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 8db6b11..49ea9be 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -2,7 +2,9 @@ import { Standards, type ActivityStandards, + type Metrics, type Standard, + type StandardsConfig, } from "$lib/services/calculator/main"; import allStandardsRaw from "$lib/data/standards.json" assert { type: "json" }; import { @@ -15,21 +17,39 @@ range, } from "$lib/services/calculator/util"; - const selected = $state({ + const input = $state({ activity: Activity.BenchPress, - gender: Gender.Male, - age: 18, - weight: 0, - maxLevel: 5, + metrics: { + gender: Gender.Male, + age: "", + weight: "", + }, + cfg: { + maxLevel: "5", + }, + }); + + const selected = $derived({ + activity: input.activity, + metrics: { + gender: input.metrics.gender, + age: +input.metrics.age, + weight: lbToKg(+input.metrics.weight), + } as Metrics, + cfg: { + maxLevel: +input.cfg.maxLevel, + } as StandardsConfig, + }); + + $effect(() => { + if (!input.metrics.age) input.metrics.weight = ""; }); const allStandards = $derived( - new Standards(allStandardsRaw as ActivityStandards, { - maxLevel: selected.maxLevel, - }), + new Standards(allStandardsRaw as ActivityStandards, selected.cfg), ); - const getFormattedLevelValue = ( + const getLvlValue = ( activity: Activity, standard: Standard, lvl: number, @@ -55,13 +75,12 @@
Metrics -
@@ -119,94 +142,77 @@ min="1" max="100" placeholder="5" - bind:value={selected.maxLevel} + bind:value={input.cfg.maxLevel} />
- {#if !selected.gender || !selected.age} -

Not enough info entered

- {:else} - {@render standardsTable( - selected.gender, - selected.age, - selected.weight, - )} - {/if} +

{allStandards.byActivity(selected.activity).getMetadata().name}

+
+

{selected.metrics.gender}

+ {#if !selected.metrics.age} + {#each allStandards.agesFor(selected.activity, selected.metrics.gender) as age} + {@render standardsTable(selected.activity, { + ...selected.metrics, + age, + })} + {/each} + {:else} + {@render standardsTable(selected.activity, selected.metrics)} + {/if} +
-{#snippet standardsTable(gender: Gender, age: number, weight: number)} -
-
-
-
- - - - - {#each range(selected.maxLevel) as lvl} - - {/each} - - - - {#if weight} - {@const standard = allStandards - .byActivity(selected.activity) - .byGender(gender) - .byAge(age) - .byWeight(lbToKg(weight)) - .getOneInterpolated()} - - - - {#each range(selected.maxLevel) as lvl} - - {/each} - - {:else} - {@const standards = allStandards - .byActivity(selected.activity) - .byGender(gender) - .byAge(age) - .getAllInterpolated()} - {#each standards as standard} - - - {#each range(allStandards.cfg.maxLevel) as lvl} - - {/each} - - {/each} - {/if} - -
Body weight{lvl}
{Math.round( - kgToLb(standard.metrics.weight), - )}{getFormattedLevelValue( - selected.activity, - standard, - lvl, - )}
{Math.round( - kgToLb(standard.metrics.weight), - )}{getFormattedLevelValue( - selected.activity, - standard, - lvl, - )}
-
+{#snippet standardsTable(activity: Activity, metrics: Metrics)} + {#snippet standardsTableRow(standard: Standard)} + + {parseFloat(kgToLb(standard.metrics.weight).toFixed(2))} + {#each range(selected.cfg.maxLevel) as lvl} + {getLvlValue(activity, standard, lvl)} + {/each} + + {/snippet} +
+

{metrics.age}

+
+
+ + + + + {#each range(selected.cfg.maxLevel) as lvl} + + {/each} + + + + {#if metrics.weight} + {@const standard = allStandards + .byActivity(activity) + .byGender(metrics.gender) + .byAge(metrics.age) + .byWeight(metrics.weight) + .getOneInterpolated()} + {@render standardsTableRow(standard)} + {:else} + {@const standards = allStandards + .byActivity(activity) + .byGender(metrics.gender) + .byAge(metrics.age) + .getAllInterpolated({ normalizeForLb: true })} + {#each standards as standard} + {@render standardsTableRow(standard)} + {/each} + {/if} + +
Body weight{lvl}
-
+ {/snippet}