Created
June 14, 2020 05:52
-
-
Save jeremyorme/66a4fa4ca876ed410b5d448780f7e0cd to your computer and use it in GitHub Desktop.
Veebe version 2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.rda-progress { | |
position: relative; | |
display: block; | |
height: 25px; | |
background: rgb(241,241,252); | |
background: linear-gradient(90deg, rgba(251,251,252,1) 0%, rgba(218,221,236,1) 100%); | |
overflow: hidden; | |
} | |
.nutrient-container { | |
position: relative; | |
width: 100%; | |
} | |
.nutrient-element { | |
padding-left: 5px; | |
padding-top: 2px; | |
position: absolute; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Component, h, State} from '@stencil/core'; | |
interface IFoodNutrient { | |
name: string; | |
unitName: string; | |
amount: number; | |
} | |
interface IFood { | |
fdc_id: string; | |
description: string; | |
foodNutrients: IFoodNutrient[]; | |
} | |
interface IIngredientQuantity { | |
name: string; | |
number: number; | |
unit: string; | |
componentIds: string[]; | |
unitMass: number; | |
density: number; | |
} | |
interface IExpandable { | |
expanded?: boolean; | |
} | |
interface IIngredientQuantityEnriched extends IIngredientQuantity, IExpandable { | |
components: IFood[]; | |
grams: number; | |
gramsExplain: string; | |
nutrientDensity: Map<string, number>; | |
nutrientGrams: Map<string, number>; | |
} | |
interface INutrientValue { | |
name: string; | |
value: number; | |
unit: string; | |
} | |
interface IResults { | |
classified: IIngredientQuantityEnriched[]; | |
aggregated: INutrientValue[]; | |
} | |
interface IRda { | |
minAge?: number; | |
maxAge?: number; | |
gender?: string; | |
eq?: number; | |
lt?: number; | |
gt?: number; | |
} | |
interface INutrientDef extends IExpandable { | |
name: string; | |
fdcName?: string; | |
rdas?: IRda[]; | |
children?: INutrientDef[]; | |
} | |
interface IAgeRange { | |
minAge?: number; | |
maxAge?: number; | |
} | |
const age1 = {minAge: 1, maxAge: 1}; | |
const age2To3 = {minAge: 2, maxAge: 3}; | |
const age4To6 = {minAge: 4, maxAge: 6}; | |
const age7To10 = {minAge: 7, maxAge: 10}; | |
const age11To14 = {minAge: 11, maxAge: 14}; | |
const age15To18 = {minAge: 15, maxAge: 18}; | |
const age19To49 = {minAge: 19, maxAge: 49}; | |
const age50To64 = {minAge: 50, maxAge: 64}; | |
const age65To74 = {minAge: 65, maxAge: 74}; | |
const age75Plus = {minAge: 75}; | |
@Component({ | |
tag: 'app-home', | |
styleUrl: 'app-home.css' | |
}) | |
export class AppHome { | |
@State() loading: boolean = false; | |
@State() recipe: string = '2 courgettes\n1 carrot\n1 avocado'; | |
@State() results: IResults = {classified: [], aggregated: []}; | |
@State() gender: string = 'Male'; | |
@State() ageRange: IAgeRange = age19To49; | |
@State() portions: number = 1; | |
units = { | |
g: { factor: 1.0, base_unit: 'g' }, | |
oz: { factor: 28.3495, base_unit: 'g' }, | |
lb: { factor: 453.592, base_unit: 'g' }, | |
kg: { factor: 1000.0, base_unit: 'g' }, | |
ml: { factor: 1.0, base_unit: 'ml' }, | |
cc: { factor: 1.0, base_unit: 'ml' }, | |
pinch: { factor: 0.73992, base_unit: 'ml' }, | |
tsp: { factor: 5.91939, base_unit: 'ml' }, | |
tbsp: { factor: 17.7582, base_unit: 'ml' }, | |
floz: { factor: 28.4131, base_unit: 'ml' }, | |
cup: { factor: 284.131, base_unit: 'ml' }, | |
pt: { factor: 568.261, base_unit: 'ml' }, | |
dl: { factor: 100.0, base_unit: 'ml' }, | |
l: { factor: 1000.0, base_unit: 'ml' }, | |
gal: { factor: 4546.09, base_unit: 'ml' } | |
}; | |
// https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/618167/government_dietary_recommendations.pdf | |
nutrients: INutrientDef[] = [ | |
{name: 'Macronutrients', expanded: true, children: [ | |
{name: 'Protein', fdcName: 'Protein', rdas: [ | |
{minAge: 1, maxAge: 3, eq: 14.5}, | |
{minAge: 4, maxAge: 6, eq: 19.7}, | |
{minAge: 7, maxAge: 10, eq: 28.3}, | |
{minAge: 11, maxAge: 14, gender: 'Male', eq: 42.1}, | |
{minAge: 11, maxAge: 14, gender: 'Female', eq: 41.2}, | |
{minAge: 15, maxAge: 18, gender: 'Male', eq: 55.2}, | |
{minAge: 15, maxAge: 64, gender: 'Female', eq: 45.0}, | |
{minAge: 19, maxAge: 64, gender: 'Male', eq: 55.5}, | |
{minAge: 65, gender: 'Male', eq: 53.3}, | |
{minAge: 65, gender: 'Female', eq: 46.5} | |
]}, | |
{name: 'Fat', fdcName: 'Total lipid (fat)', rdas: [ | |
{minAge: 4, maxAge: 6, gender: 'Male', lt: 58.0}, | |
{minAge: 4, maxAge: 6, gender: 'Female', lt: 54.0}, | |
{minAge: 7, maxAge: 10, gender: 'Male', lt: 71.0}, | |
{minAge: 7, maxAge: 10, gender: 'Female', lt: 66.0}, | |
{minAge: 11, maxAge: 64, gender: 'Male', lt: 97.0}, | |
{minAge: 11, maxAge: 64, gender: 'Female', lt: 78.0}, | |
{minAge: 65, maxAge: 74, gender: 'Male', lt: 91.0}, | |
{minAge: 65, maxAge: 74, gender: 'Female', lt: 74.0}, | |
{minAge: 75, gender: 'Male', lt: 89.0}, | |
{minAge: 75, gender: 'Female', lt: 72.0} | |
], children: [ | |
{name: 'Saturated fat', fdcName: 'Fatty acids, total saturated', rdas: [ | |
{minAge: 4, maxAge: 6, gender: 'Male', lt: 18.0}, | |
{minAge: 4, maxAge: 6, gender: 'Female', lt: 17.0}, | |
{minAge: 7, maxAge: 10, gender: 'Male', lt: 22.0}, | |
{minAge: 7, maxAge: 10, gender: 'Female', lt: 21.0}, | |
{minAge: 11, maxAge: 64, gender: 'Male', lt: 31.0}, | |
{minAge: 11, maxAge: 64, gender: 'Female', lt: 24.0}, | |
{minAge: 65, maxAge: 74, gender: 'Male', lt: 29.0}, | |
{minAge: 65, gender: 'Female', lt: 23.0}, | |
{minAge: 75, gender: 'Male', lt: 28.0} | |
]}, | |
{name: 'Monounsaturated fat', fdcName: 'Fatty acids, total monounsaturated', rdas: [ | |
{minAge: 4, maxAge: 6, gender: 'Male', eq: 21.0}, | |
{minAge: 4, maxAge: 6, gender: 'Female', eq: 20.0}, | |
{minAge: 7, maxAge: 10, gender: 'Male', eq: 26.0}, | |
{minAge: 7, maxAge: 10, gender: 'Female', eq: 25.0}, | |
{minAge: 11, maxAge: 64, gender: 'Male', eq: 36.0}, | |
{minAge: 11, maxAge: 64, gender: 'Female', eq: 29.0}, | |
{minAge: 65, maxAge: 74, gender: 'Male', eq: 34.0}, | |
{minAge: 65, maxAge: 74, gender: 'Female', eq: 28.0}, | |
{minAge: 75, gender: 'Male', eq: 33.0}, | |
{minAge: 75, gender: 'Female', eq: 27.0} | |
]}, | |
{name: 'Polyunsaturated fat', fdcName: 'Fatty acids, total polyunsaturated', rdas: [ | |
{minAge: 4, maxAge: 6, gender: 'Male', eq: 11.0}, | |
{minAge: 4, maxAge: 6, gender: 'Female', eq: 10.0}, | |
{minAge: 7, maxAge: 10, gender: 'Male', eq: 13.0}, | |
{minAge: 7, maxAge: 10, gender: 'Female', eq: 12.0}, | |
{minAge: 11, maxAge: 64, gender: 'Male', eq: 18.0}, | |
{minAge: 11, maxAge: 74, gender: 'Female', eq: 14.0}, | |
{minAge: 65, gender: 'Male', eq: 17.0}, | |
{minAge: 75, gender: 'Female', eq: 13.0} | |
]}, | |
]}, | |
{name: 'Carbohydrate', fdcName: 'Carbohydrate, by difference', rdas: [ | |
{minAge: 2, maxAge: 3, gender: 'Male', gt: 145.0}, | |
{minAge: 2, maxAge: 3, gender: 'Female', gt: 134.0}, | |
{minAge: 4, maxAge: 6, gender: 'Male', gt: 198.0}, | |
{minAge: 4, maxAge: 6, gender: 'Female', gt: 184.0}, | |
{minAge: 7, maxAge: 10, gender: 'Male', gt: 242.0}, | |
{minAge: 7, maxAge: 10, gender: 'Female', gt: 227.0}, | |
{minAge: 11, maxAge: 64, gender: 'Male', gt: 333.0}, | |
{minAge: 11, maxAge: 64, gender: 'Female', gt: 267.0}, | |
{minAge: 65, maxAge: 74, gender: 'Male', gt: 312.0}, | |
{minAge: 65, maxAge: 74, gender: 'Female', gt: 255.0}, | |
{minAge: 75, gender: 'Male', gt: 306.0}, | |
{minAge: 75, gender: 'Female', gt: 245.0} | |
]}, | |
{name: 'Dietary fibre', fdcName: 'Fiber, total dietary', rdas: [ | |
{minAge: 2, maxAge: 4, lt: 15.0}, | |
{minAge: 5, maxAge: 10, lt: 20.0}, | |
{minAge: 11, maxAge: 14, lt: 25.0}, | |
{minAge: 15, lt: 30.0} | |
]}, | |
]}, | |
{name: 'Vitamins', expanded: true, children: [ | |
{name: 'Vitamin A', fdcName: 'Vitamin A, RAE', rdas: [ | |
{minAge: 1, maxAge: 6, eq: 400.0}, | |
{minAge: 7, maxAge: 10, eq: 500.0}, | |
{minAge: 11, maxAge: 14, gender: 'Male', eq: 600.0}, | |
{minAge: 11, gender: 'Female', eq: 600.0}, | |
{minAge: 15, gender: 'Male', eq: 700.0} | |
]}, | |
{name: 'Thiamin', fdcName: 'Thiamin', rdas: [ | |
{minAge: 1, maxAge: 1, eq: 0.3}, | |
{minAge: 2, maxAge: 3, eq: 0.4}, | |
{minAge: 4, maxAge: 6, eq: 0.6}, | |
{minAge: 7, maxAge: 10, eq: 0.7}, | |
{minAge: 11, maxAge: 64, gender: 'Male', eq: 1.0}, | |
{minAge: 11, maxAge: 74, gender: 'Female', eq: 0.8}, | |
{minAge: 65, gender: 'Male', eq: 0.9}, | |
{minAge: 75, gender: 'Female', eq: 0.7} | |
]}, | |
{name: 'Riboflavin', fdcName: 'Riboflavin', rdas: [ | |
{minAge: 1, maxAge: 3, eq: 0.6}, | |
{minAge: 4, maxAge: 6, eq: 0.8}, | |
{minAge: 7, maxAge: 10, eq: 1.0}, | |
{minAge: 11, maxAge: 14, gender: 'Male', eq: 1.2}, | |
{minAge: 11, gender: 'Female', eq: 1.1}, | |
{minAge: 15, gender: 'Male', eq: 1.3} | |
]}, | |
{name: 'Niacin', fdcName: 'Niacin', rdas: [ | |
{minAge: 1, maxAge: 1, gender: 'Male', eq: 5.0}, | |
{minAge: 1, maxAge: 1, gender: 'Female', eq: 4.7}, | |
{minAge: 2, maxAge: 3, gender: 'Male', eq: 7.2}, | |
{minAge: 2, maxAge: 3, gender: 'Female', eq: 6.6}, | |
{minAge: 4, maxAge: 6, gender: 'Male', eq: 9.8}, | |
{minAge: 4, maxAge: 6, gender: 'Female', eq: 9.1}, | |
{minAge: 7, maxAge: 10, gender: 'Male', eq: 12.0}, | |
{minAge: 7, maxAge: 10, gender: 'Female', eq: 11.2}, | |
{minAge: 11, maxAge: 64, gender: 'Male', eq: 16.5}, | |
{minAge: 11, maxAge: 64, gender: 'Female', eq: 13.2}, | |
{minAge: 65, maxAge: 74, gender: 'Male', eq: 15.5}, | |
{minAge: 65, maxAge: 74, gender: 'Female', eq: 12.6}, | |
{minAge: 75, gender: 'Male', eq: 15.1}, | |
{minAge: 75, gender: 'Female', eq: 12.1} | |
]}, | |
{name: 'Vitamin B6', fdcName: 'Vitamin B-6', rdas: [ | |
{minAge: 1, maxAge: 3, eq: 0.7}, | |
{minAge: 4, maxAge: 6, eq: 0.9}, | |
{minAge: 7, maxAge: 10, eq: 1.0}, | |
{minAge: 11, maxAge: 14, gender: 'Male', eq: 1.2}, | |
{minAge: 11, maxAge: 14, gender: 'Female', eq: 1.0}, | |
{minAge: 15, maxAge: 18, gender: 'Male', eq: 1.5}, | |
{minAge: 15, gender: 'Female', eq: 1.2}, | |
{minAge: 18, gender: 'Male', eq: 1.4} | |
]}, | |
{name: 'Vitamin B12', fdcName: 'Vitamin B-12', rdas: [ | |
{minAge: 1, maxAge: 3, eq: 0.5}, | |
{minAge: 4, maxAge: 6, eq: 0.8}, | |
{minAge: 7, maxAge: 10, eq: 1.0}, | |
{minAge: 11, maxAge: 14, eq: 1.2}, | |
{minAge: 15, eq: 1.5} | |
]}, | |
{name: 'Folate', fdcName: 'Folate, DFE', rdas: [ | |
{minAge: 1, maxAge: 3, eq: 70.0}, | |
{minAge: 4, maxAge: 6, eq: 100.0}, | |
{minAge: 7, maxAge: 10, eq: 150.0}, | |
{minAge: 11, eq: 200.0} | |
]}, | |
{name: 'Vitamin C', fdcName: 'Vitamin C, total ascorbic acid', rdas: [ | |
{minAge: 1, maxAge: 10, eq: 30.0}, | |
{minAge: 11, maxAge: 14, eq: 35.0}, | |
{minAge: 15, eq: 40.0} | |
]}, | |
{name: 'Vitamin D', fdcName: 'Vitamin D (D2 + D3)', rdas: [ | |
{minAge: 1, eq: 10.0} | |
]} | |
]}, | |
{name: 'Minerals', expanded: true, children: [ | |
{name: 'Iron', fdcName: 'Iron, Fe', rdas: [ | |
{minAge: 1, maxAge: 3, eq: 6.9}, | |
{minAge: 4, maxAge: 6, eq: 6.1}, | |
{minAge: 7, maxAge: 10, eq: 8.7}, | |
{minAge: 11, maxAge: 18, gender: 'Male', eq: 11.3}, | |
{minAge: 11, maxAge: 49, gender: 'Female', eq: 14.8}, | |
{minAge: 19, gender: 'Male', eq: 8.7}, | |
{minAge: 50, gender: 'Female', eq: 8.7} | |
]}, | |
{name: 'Calcium', fdcName: 'Calcium, Ca', rdas: [ | |
{minAge: 1, maxAge: 3, eq: 350.0}, | |
{minAge: 4, maxAge: 6, eq: 450.0}, | |
{minAge: 7, maxAge: 10, eq: 550.0}, | |
{minAge: 11, maxAge: 18, gender: 'Male', eq: 1000.0}, | |
{minAge: 11, maxAge: 18, gender: 'Female', eq: 800.0}, | |
{minAge: 19, eq: 700.0} | |
]}, | |
{name: 'Magnesium', fdcName: 'Magnesium, Mg', rdas: [ | |
{minAge: 1, maxAge: 3, eq: 85.0}, | |
{minAge: 4, maxAge: 6, eq: 120.0}, | |
{minAge: 7, maxAge: 10, eq: 200.0}, | |
{minAge: 11, maxAge: 14, eq: 280.0}, | |
{minAge: 15, maxAge: 18, eq: 300.0}, | |
{minAge: 19, gender: 'Male', eq: 300.0}, | |
{minAge: 19, gender: 'Female', eq: 270.0} | |
]}, | |
{name: 'Potassium', fdcName: 'Potassium, K', rdas: [ | |
{minAge: 1, maxAge: 3, eq: 800.0}, | |
{minAge: 4, maxAge: 6, eq: 1100.0}, | |
{minAge: 7, maxAge: 10, eq: 2000.0}, | |
{minAge: 11, maxAge: 14, eq: 3100.0}, | |
{minAge: 15, eq: 3500.0} | |
]}, | |
{name: 'Zinc', fdcName: 'Zinc, Zn', rdas: [ | |
{minAge: 1, maxAge: 3, eq: 5.0}, | |
{minAge: 4, maxAge: 6, eq: 6.5}, | |
{minAge: 7, maxAge: 10, eq: 7.0}, | |
{minAge: 11, maxAge: 14, eq: 9.0}, | |
{minAge: 15, gender: 'Male', eq: 9.5}, | |
{minAge: 15, gender: 'Female', eq: 7.0} | |
]}, | |
{name: 'Copper', fdcName: 'Copper, Cu', rdas: [ | |
{minAge: 1, maxAge: 3, eq: 0.4}, | |
{minAge: 4, maxAge: 6, eq: 0.6}, | |
{minAge: 7, maxAge: 10, eq: 0.7}, | |
{minAge: 11, maxAge: 14, eq: 0.8}, | |
{minAge: 14, maxAge: 18, eq: 1.0}, | |
{minAge: 19, eq: 1.2} | |
]}, | |
{name: 'Iodine', fdcName: 'Iodine, I', rdas: [ | |
{minAge: 1, maxAge: 3, eq: 70.0}, | |
{minAge: 4, maxAge: 6, eq: 100.0}, | |
{minAge: 7, maxAge: 10, eq: 110.0}, | |
{minAge: 11, maxAge: 14, eq: 130.0}, | |
{minAge: 15, eq: 140.0} | |
]}, | |
{name: 'Selenium', fdcName: 'Selenium, Se', rdas: [ | |
{minAge: 1, maxAge: 3, eq: 15.0}, | |
{minAge: 4, maxAge: 6, eq: 20.0}, | |
{minAge: 7, maxAge: 10, eq: 30.0}, | |
{minAge: 11, maxAge: 14, eq: 45.0}, | |
{minAge: 15, maxAge: 18, gender: 'Male', eq: 70.0}, | |
{minAge: 15, maxAge: 18, gender: 'Female', eq: 60.0} | |
]}, | |
{name: 'Phosphorus', fdcName: 'Phosphorus, P', rdas: [ | |
{minAge: 1, maxAge: 3, eq: 270.0}, | |
{minAge: 4, maxAge: 6, eq: 350.0}, | |
{minAge: 7, maxAge: 10, eq: 450.0}, | |
{minAge: 11, maxAge: 18, gender: 'Male', eq: 775.0}, | |
{minAge: 11, maxAge: 18, gender: 'Female', eq: 625.0}, | |
{minAge: 19, eq: 550.0} | |
]}, | |
{name: 'Chloride', fdcName: 'Chlorine, Cl', rdas: [ | |
{minAge: 1, maxAge: 3, eq: 800.0}, | |
{minAge: 4, maxAge: 6, eq: 1100.0}, | |
{minAge: 7, maxAge: 10, eq: 1800.0}, | |
{minAge: 11, eq: 2500.0} | |
]}, | |
{name: 'Sodium', fdcName: 'Sodium, Na', rdas: [ | |
{minAge: 1, maxAge: 3, lt: 800}, | |
{minAge: 4, maxAge: 6, lt: 1200}, | |
{minAge: 7, maxAge: 10, lt: 2000}, | |
{minAge: 11, lt: 2400} | |
]} | |
]} | |
]; | |
static async fetchFood(fdcId: string): Promise<IFood> { | |
let res = await fetch('https://api.nal.usda.gov/fdc/v1/food/' + fdcId + '?api_key=y14zokqvhjz7HfuBypJkYwKwknfKPDowl75n9rRt&format=abridged', { | |
method: 'GET' | |
}); | |
return res.json(); | |
} | |
static async fetchClassified(iqStrings: string[]): Promise<IIngredientQuantity[]> { | |
let res = await fetch('https://us-central1-nutrition-calculator-264017.cloudfunctions.net/nutrient-table', { | |
method: 'POST', | |
headers: {'Content-Type': 'application/json'}, | |
body: JSON.stringify({recipe: iqStrings}) | |
}); | |
return res.json(); | |
} | |
calcAggregated(classified: IIngredientQuantityEnriched[]): INutrientValue[] { | |
// Calculate the mass in grams of each ingredient | |
for (let iq of classified) { | |
if (iq.componentIds.length == 0) { | |
iq.grams = 0.0; | |
iq.gramsExplain = 'unrecognised ingredient'; | |
continue; | |
} | |
if (iq.unit == 'default') { | |
iq.grams = iq.number * iq.unitMass / this.portions; | |
iq.gramsExplain = iq.number + ' * ' + iq.unitMass + 'g' + ' / ' + this.portions; | |
} | |
else if (this.units[iq.unit]) { | |
if (this.units[iq.unit].base_unit == 'g') { | |
iq.grams = iq.number * this.units[iq.unit].factor / this.portions; | |
iq.gramsExplain = iq.number + ' ' + iq.unit + ' * ' + this.units[iq.unit].factor + ' g/' + iq.unit + ' / ' + this.portions; | |
} | |
else { | |
iq.grams = iq.number * this.units[iq.unit].factor * iq.density / this.portions; | |
iq.gramsExplain = iq.number + ' ' + iq.unit + ' * ' + this.units[iq.unit].factor + ' ml/' + iq.unit + ' * ' + iq.density + ' g/ml' + ' / ' + this.portions; | |
} | |
} | |
else { | |
iq.grams = 0.0; | |
iq.gramsExplain = 'unrecognised unit'; | |
} | |
} | |
// Find the number of ingredients containing each nutrient and the unit the nutrient is denominated in | |
let componentCount = 0; | |
let nutrientCounts: Map<string, number> = new Map(); | |
let nutrientUnits: Map<string, string> = new Map(); | |
for (let iq of classified) | |
for (let c of iq.components) { | |
componentCount++; | |
for (let n of c.foodNutrients) { | |
nutrientCounts.set(n.name, nutrientCounts.get(n.name) ? nutrientCounts.get(n.name) + 1 : 1); | |
nutrientUnits.set(n.name, n.unitName); | |
} | |
} | |
// Find nutrients that have RDAs | |
const defs: INutrientDef[] = [...this.nutrients]; | |
let fdcNames: Set<string> = new Set(); | |
while (defs.length > 0) { | |
const def = defs.pop(); | |
if (def.fdcName) | |
fdcNames.add(def.fdcName); | |
if (def.children) | |
defs.push(...def.children); | |
} | |
// For nutrients contained in every ingredient, add an aggregate entry with zero initial value | |
let aggregated: INutrientValue[] = []; | |
for (let [n, c] of nutrientCounts.entries()) | |
if (c == componentCount && fdcNames.has(n)) | |
aggregated.push({name: n, value: 0.0, unit: nutrientUnits.get(n)}); | |
// Calculate the mass of each nutrient in each ingredient in grams and aggregate over the ingredients | |
for (let iq of classified) { | |
iq.nutrientDensity = new Map(); | |
iq.nutrientGrams = new Map(); | |
for (let n of aggregated) { | |
let sum = 0.0; | |
for (let c of iq.components) { | |
for (let n2 of c.foodNutrients) | |
if (n.name == n2.name) | |
sum += n2.amount; | |
} | |
const nutrientDensity = sum / iq.components.length; | |
iq.nutrientDensity.set(n.name, nutrientDensity); | |
const nutrientGrams = nutrientDensity / 100 * iq.grams; | |
iq.nutrientGrams.set(n.name, nutrientGrams); | |
n.value += nutrientGrams; | |
} | |
} | |
return aggregated; | |
} | |
async classifyAndAggregate() { | |
// Invoke cloud function to classify raw ingredient strings | |
let classified = (await AppHome.fetchClassified(this.recipe.split('\n'))) as IIngredientQuantityEnriched[]; | |
// Fetch FDC data for identified food components | |
await Promise.all( | |
classified.map(async iq => { | |
iq.components = await Promise.all( | |
iq.componentIds.map(cid => AppHome.fetchFood(cid))) | |
})); | |
const aggregated = this.calcAggregated(classified); | |
// Update the displayed data | |
this.results = { classified: classified, aggregated: aggregated }; | |
} | |
aggregate() { | |
this.results = { classified: this.results.classified, aggregated: this.calcAggregated(this.results.classified) }; | |
} | |
async updateClicked() { | |
this.loading = true; | |
await this.classifyAndAggregate(); | |
this.loading = false; | |
} | |
effectiveRda(nutrientDef: INutrientDef) { | |
for (const rda of nutrientDef.rdas) { | |
if (rda.gender && this.gender != rda.gender) | |
continue; | |
if (rda.minAge && this.ageRange.minAge < rda.minAge) | |
continue; | |
if (rda.maxAge && this.ageRange.maxAge > rda.maxAge) | |
continue; | |
if (rda.eq) | |
return rda.eq; | |
if (rda.lt) | |
return rda.lt; | |
if (rda.gt) | |
return rda.gt; | |
} | |
} | |
calcRda(result: INutrientValue, nutrientDefs: INutrientDef[]) { | |
for (const nutrientDef of nutrientDefs) { | |
if (result.name == nutrientDef.fdcName) { | |
const rda = this.effectiveRda(nutrientDef); | |
if (rda || rda == 0.0) | |
return rda; | |
} | |
if (nutrientDef.children) { | |
const rda = this.calcRda(result, nutrientDef.children); | |
if (rda || rda == 0.0) | |
return rda; | |
} | |
} | |
} | |
buildNutrientRdaTitle(name: string, result: INutrientValue) { | |
const rda = this.calcRda(result, this.nutrients); | |
return rda || rda == 0.0 ? <span class="nutrient-element">{name} = {result.value.toFixed(1)} {result.unit.toLowerCase()} ({(result.value * 100 / rda).toFixed(0)}% RDA)</span> : <span class="nutrient-element">RDA unavailable</span>; | |
} | |
buildRdaProgress(result: INutrientValue) { | |
const rda = this.calcRda(result, this.nutrients); | |
return rda || rda == 0.0 ? <span class="nutrient-element rda-progress" style={{width: (result.value * 100 / rda).toFixed(0) + '%'}}/> : <span />; | |
} | |
buildNutrientTitle(n: INutrientDef) { | |
if (!n.fdcName) | |
return <span>{n.name}</span>; | |
for (const result of this.results.aggregated) { | |
if (result.name == n.fdcName) { | |
return <span class="nutrient-container">{this.buildRdaProgress(result)}{this.buildNutrientRdaTitle(n.name, result)}</span>; | |
} | |
} | |
return <span>{n.name} = (no data)</span>; | |
} | |
buildComponentBreakdown(nutrientDef: INutrientDef) { | |
if (!nutrientDef.fdcName) | |
return null; | |
for (const result of this.results.aggregated) | |
if (result.name == nutrientDef.fdcName) | |
return this.results.classified.map(iq => <ion-item> | |
{iq.number} {iq.unit == 'default' ? 'x' : iq.unit.toLowerCase()} {iq.name}: ( | |
{iq.nutrientDensity.get(result.name).toFixed(2)} {result.unit.toLowerCase()}/100g / 100 * {iq.grams.toFixed(2)} g = {iq.nutrientGrams.get(result.name).toFixed(2)} {result.unit.toLowerCase()})</ion-item>); | |
} | |
toggleExpanded(n: IExpandable) { | |
n.expanded = n.expanded ? false : true; | |
this.results = {...this.results}; | |
} | |
buildNutrientListItem(n: INutrientDef, level: number) { | |
const title = this.buildNutrientTitle(n); | |
const content = n.expanded ? <div>{this.buildComponentBreakdown(n)}{n.children ? n.children.map(c => this.buildNutrientListItem(c, level + 1)) : null}</div> : null; | |
if (level == 1) | |
return <div><h2>{title}</h2>{content}</div>; | |
return <div><ion-item onClick={() => this.toggleExpanded(n)}>{title}</ion-item>{content}</div>; | |
} | |
render() { | |
return [ | |
<ion-header> | |
<ion-toolbar color="primary"> | |
<ion-title>Veebe</ion-title> | |
</ion-toolbar> | |
</ion-header>, | |
<ion-content class="ion-padding"> | |
<ion-textarea value={this.recipe} onIonChange={e => {this.recipe = e.detail.value}} rows={10} /> | |
<ion-button onClick={() => this.updateClicked()}>Update</ion-button> | |
<ion-item> | |
<ion-label>Gender</ion-label> | |
<ion-select value={this.gender} onIonChange={e => {this.gender = e.detail.value}}> | |
<ion-select-option>Male</ion-select-option> | |
<ion-select-option>Female</ion-select-option> | |
</ion-select> | |
</ion-item> | |
<ion-item> | |
<ion-label>Age</ion-label> | |
<ion-select value={this.ageRange} onIonChange={e => {this.ageRange = e.detail.value}}> | |
<ion-select-option value={age1}>1</ion-select-option> | |
<ion-select-option value={age2To3}>2 - 3</ion-select-option> | |
<ion-select-option value={age4To6}>4 - 6</ion-select-option> | |
<ion-select-option value={age7To10}>7 - 10</ion-select-option> | |
<ion-select-option value={age11To14}>11 - 14</ion-select-option> | |
<ion-select-option value={age15To18}>15 - 18</ion-select-option> | |
<ion-select-option value={age19To49}>19 - 49</ion-select-option> | |
<ion-select-option value={age50To64}>50 - 64</ion-select-option> | |
<ion-select-option value={age65To74}>65 - 74</ion-select-option> | |
<ion-select-option value={age75Plus}>75+</ion-select-option> | |
</ion-select> | |
</ion-item> | |
<ion-item> | |
<ion-label>Portions</ion-label> | |
<ion-select value={this.portions} onIonChange={e => {this.portions = e.detail.value; this.aggregate();}}> | |
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(p => <ion-select-option value={p}>{p}</ion-select-option>)} | |
</ion-select> | |
</ion-item> | |
{this.loading ? <p>Loading<br/><ion-spinner name="dots"/></p> : | |
<div> | |
{this.results.aggregated.length > 0 ? <div> | |
<h2>Matched Ingredients</h2> | |
{this.results.classified.map(iq => | |
<div> | |
<ion-item onClick={() => this.toggleExpanded(iq)}>{iq.number} {iq.unit == 'default' ? 'x' : iq.unit} {iq.name}</ion-item> | |
{iq.expanded ? <div> | |
<ion-item>Mass = {iq.gramsExplain} = {iq.grams.toFixed(2)} g</ion-item> | |
{iq.density > 0.0 ? <ion-item>Density = {iq.density.toFixed(3)} g/ml</ion-item> : null} | |
{iq.unitMass > 0.0 ? <ion-item>Unit Mass = {iq.unitMass.toFixed(1)} g</ion-item> : null} | |
<ion-item>FDC Component Foods</ion-item> | |
{iq.components.map(c => <ion-item>{c.fdc_id} {c.description}</ion-item>)} | |
<ion-item>Average Nutrient Values</ion-item> | |
{this.results.aggregated.map(n => <ion-item>{n.name} = {iq.nutrientDensity.get(n.name).toFixed(2)} {n.unit.toLowerCase()} / 100g</ion-item>)} | |
</div> : null} | |
</div>)} | |
{this.nutrients.map(n => this.buildNutrientListItem(n, 1))} | |
</div> : null} | |
</div>} | |
</ion-content> | |
]; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment