356 lines
13 KiB
TypeScript
356 lines
13 KiB
TypeScript
import { defineComponent, computed, ref } from 'vue';
|
|
|
|
/*
|
|
class GrainInput {
|
|
static inputs = [];
|
|
name;
|
|
unit;
|
|
mpg;
|
|
inputEl;
|
|
reference;
|
|
|
|
* @param {{
|
|
* name: string,
|
|
* mpg: number,
|
|
* reference: GrainInput | 'self',
|
|
* unit: string,
|
|
* step?: number,
|
|
* }} opts
|
|
constructor(opts) {
|
|
this.name = opts.name;
|
|
this.mpg = opts.mpg;
|
|
this.unit = opts.unit;
|
|
this.reference = opts.reference === "self" ? this : opts.reference;
|
|
this.inputEl = h("input", { type: "number", min: 0, step: opts.step ?? 1 });
|
|
eventBus.addListener(this);
|
|
}
|
|
|
|
attachInput(insertionPoint) {
|
|
insertionPoint.appendChild(this.inputEl);
|
|
this.inputEl.valueAsNumber = this.mpg;
|
|
this.inputEl.addEventListener("input", (e) => {
|
|
const newVal = (e.currentTarget)?.valueAsNumber ?? 0;
|
|
if (this !== this.reference) {
|
|
this.reference.inputEl.valueAsNumber = newVal / this.mpg;
|
|
}
|
|
eventBus.sendChange(this);
|
|
});
|
|
}
|
|
|
|
onChange() {
|
|
if (!this.reference || this === this.reference) return;
|
|
this.inputEl.valueAsNumber = this.mpg * this.reference.inputEl.valueAsNumber;
|
|
}
|
|
|
|
getCurrentValue() {
|
|
return this.inputEl.valueAsNumber;
|
|
}
|
|
}
|
|
|
|
class RatiosController {
|
|
t3Ratio = h("input", { type: "number", min: 0, step: 1 });
|
|
t4Ratio = h("input", { type: "number", min: 0, step: 1 });
|
|
t3Syn = h("input", { type: "number", min: 0 });
|
|
t4Syn = h("input", { type: "number", min: 0 });
|
|
t3 = 1;
|
|
t4 = 4;
|
|
reference;
|
|
|
|
constructor(referenceInput) {
|
|
this.reference = referenceInput;
|
|
}
|
|
|
|
attachInputs(inputs) {
|
|
inputs.t3Ratio.replaceWith(this.t3Ratio);
|
|
inputs.t4Ratio.replaceWith(this.t4Ratio);
|
|
inputs.t3Syn.replaceWith(this.t3Syn);
|
|
inputs.t4Syn.replaceWith(this.t4Syn);
|
|
|
|
this.t3Ratio.addEventListener("input", (e) => {
|
|
const newVal = (e.currentTarget)?.valueAsNumber ?? 0;
|
|
this.t3 = newVal;
|
|
this.onChange();
|
|
});
|
|
|
|
this.t4Ratio.addEventListener("input", (e) => {
|
|
const newVal = (e.currentTarget)?.valueAsNumber ?? 0;
|
|
this.t4 = newVal;
|
|
this.onChange();
|
|
});
|
|
|
|
this.t3Syn.addEventListener("input", (e) => {
|
|
const newVal = (e.currentTarget)?.valueAsNumber ?? 0;
|
|
this.reference.inputEl.valueAsNumber = newVal / MPG_T3_SYN + this.t4Syn.valueAsNumber / MPG_T4_SYN;
|
|
this.t3 = newVal;
|
|
this.t4 = this.t4Syn.valueAsNumber;
|
|
this.updateRatio();
|
|
this.updateSyn();
|
|
eventBus.sendChange(this);
|
|
});
|
|
|
|
this.t4Syn.addEventListener("input", (e) => {
|
|
const newVal = (e.currentTarget)?.valueAsNumber ?? 0;
|
|
this.reference.inputEl.valueAsNumber = newVal / MPG_T4_SYN + this.t3Syn.valueAsNumber / MPG_T3_SYN;
|
|
this.t3 = this.t3Syn.valueAsNumber;
|
|
this.t4 = newVal;
|
|
this.updateRatio();
|
|
this.updateSyn();
|
|
eventBus.sendChange(this);
|
|
});
|
|
|
|
eventBus.addListener(this);
|
|
this.onChange();
|
|
}
|
|
|
|
onChange() {
|
|
this.updateRatio();
|
|
this.updateSyn();
|
|
}
|
|
|
|
updateRatio() {
|
|
this.t3Ratio.valueAsNumber = this.t3;
|
|
this.t4Ratio.valueAsNumber = this.t4;
|
|
}
|
|
|
|
updateSyn() {
|
|
const total = this.t3 + this.t4;
|
|
const t3Proportion = this.t3 / total;
|
|
const t4Proportion = this.t4 / total;
|
|
const grainsSyn = t3Proportion / MPG_T3_SYN + t4Proportion / MPG_T4_SYN;
|
|
const multiplierSyn = this.reference.getCurrentValue() / grainsSyn;
|
|
this.t3Syn.valueAsNumber = t3Proportion * multiplierSyn;
|
|
this.t4Syn.valueAsNumber = t4Proportion * multiplierSyn;
|
|
}
|
|
}
|
|
|
|
class ThyroidConverter extends DJElement {
|
|
static styles = css``;
|
|
|
|
static template = html;
|
|
|
|
constructor() {
|
|
super();
|
|
const mainGrainInput = new GrainInput({
|
|
name: "Grains",
|
|
mpg: 1,
|
|
reference: "self",
|
|
unit: "",
|
|
step: 0.1,
|
|
});
|
|
|
|
const ratiosController = new RatiosController(mainGrainInput);
|
|
|
|
const inputs = [
|
|
mainGrainInput,
|
|
new GrainInput({
|
|
name: "Armour, Natural Dessicated Thyroid",
|
|
mpg: 60,
|
|
unit: "mg",
|
|
reference: mainGrainInput,
|
|
}),
|
|
new GrainInput({
|
|
name: 'Liothyronine (Triiodothyronine, "Cytomel", T3)',
|
|
mpg: MPG_T3_SYN,
|
|
unit: "mcg",
|
|
reference: mainGrainInput,
|
|
}),
|
|
new GrainInput({
|
|
name: 'Levothyroxine (Thyroxine, "Cynoplus", T4)',
|
|
mpg: MPG_T4_SYN,
|
|
unit: "mcg",
|
|
reference: mainGrainInput,
|
|
}),
|
|
];
|
|
|
|
const tableBody = qs("#table-body", this.root);
|
|
const compoundedStart = qs("#compounded-start", this.root);
|
|
|
|
for (const field of inputs) {
|
|
const newRow = h("tr", {}, [
|
|
h("td", { textContent: field.name }),
|
|
h("td", {}, [
|
|
h("div", { className: "input", style: "display: inline-block;" }),
|
|
h("span", { className: "breathe", textContent: field.unit }),
|
|
]),
|
|
]);
|
|
field.attachInput(qs("div.input", newRow));
|
|
tableBody.insertBefore(newRow, compoundedStart);
|
|
}
|
|
|
|
ratiosController.attachInputs({
|
|
t3Ratio: qs(".ratios .t3", tableBody),
|
|
t4Ratio: qs(".ratios .t4", tableBody),
|
|
t3Syn: qs(".synthetic .t3", tableBody),
|
|
t4Syn: qs(".synthetic .t4", tableBody),
|
|
});
|
|
}
|
|
}
|
|
*/
|
|
|
|
export default defineComponent({
|
|
name: 'ge-calculator',
|
|
setup() {
|
|
const t3Ratio = ref(1);
|
|
const t4Ratio = ref(4);
|
|
|
|
const MPG_T3_SYN = 25;
|
|
const MPG_T4_SYN = 100;
|
|
|
|
const inputDefs = [
|
|
{
|
|
name: "Grains",
|
|
mpg: 1,
|
|
unit: "",
|
|
step: 0.1,
|
|
},
|
|
{
|
|
name: "Armour, Natural Dessicated Thyroid",
|
|
mpg: 60,
|
|
unit: "mg",
|
|
},
|
|
{
|
|
name: 'Liothyronine (Triiodothyronine, "Cytomel", T3)',
|
|
mpg: MPG_T3_SYN,
|
|
unit: "mcg",
|
|
},
|
|
{
|
|
name: 'Levothyroxine (Thyroxine, "Cynoplus", T4)',
|
|
mpg: MPG_T4_SYN,
|
|
unit: "mcg",
|
|
},
|
|
];
|
|
|
|
const numGrains = ref(2);
|
|
|
|
const ratio = computed(() => t3Ratio.value + t4Ratio.value);
|
|
const compounded = computed(() => {
|
|
const total = t3Ratio.value + t4Ratio.value;
|
|
const t3Proportion = t3Ratio.value / total;
|
|
const t4Proportion = t4Ratio.value / total;
|
|
const grainsSyn = t3Proportion / MPG_T3_SYN + t4Proportion / MPG_T4_SYN;
|
|
const multiplierSyn = numGrains.value / grainsSyn;
|
|
return {
|
|
t3Syn: t3Proportion * multiplierSyn,
|
|
t4Syn: t4Proportion * multiplierSyn,
|
|
};
|
|
});
|
|
|
|
return () => <>
|
|
<header>
|
|
<h1>Thyroid Calculator</h1>
|
|
</header>
|
|
<div class="text-slab">
|
|
<p>
|
|
Use this calculator to convert between different forms and units of thyroid hormone. Common
|
|
synthetic, pure and natural dessicated forms are listed. The last section of the table provides
|
|
input fields for a specified ratio of T3 to T4, and will give the dosages required for both the
|
|
synthetic and pure forms. All dosages given are based on the "Grains" field at the beginning of the
|
|
table, but editing any field will automatically update the rest to keep them in sync.
|
|
</p>
|
|
</div>
|
|
<div class="text-slab ge-calculator">
|
|
<table>
|
|
<tbody>
|
|
{ inputDefs.map((_, i) => (
|
|
<tr key={_.name}>
|
|
<td>
|
|
{ _.name }
|
|
</td>
|
|
<td class="right">
|
|
<div style="display: inline-block;">
|
|
<input
|
|
value={_.name === 'Grains' ? numGrains.value : _.mpg * numGrains.value}
|
|
onInput={(e) => {
|
|
const val = (e.target as HTMLInputElement).valueAsNumber;
|
|
if (_.name === 'Grains') {
|
|
numGrains.value = val;
|
|
} else {
|
|
numGrains.value = _.mpg / val;
|
|
}
|
|
}}
|
|
min="0"
|
|
step={ _.step ?? 1 }
|
|
type="number" />
|
|
</div>
|
|
<span class="breathe">{ _.unit }</span>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
<tr><td colspan="2"><strong>Compounded (T3 and T4, "Cynpolus")</strong></td></tr>
|
|
<tr class="ratios">
|
|
<td>
|
|
Desired Ratio (T3:T4)
|
|
</td>
|
|
<td class="right">
|
|
<div>
|
|
<input
|
|
value={t3Ratio.value}
|
|
onInput={(e) => {
|
|
t3Ratio.value = (e.currentTarget as HTMLInputElement).valueAsNumber;
|
|
}}
|
|
min="0"
|
|
step="1"
|
|
type="number" />
|
|
</div> : <div>
|
|
<input
|
|
value={t4Ratio.value}
|
|
onInput={(e) => {
|
|
t4Ratio.value = (e.currentTarget as HTMLInputElement).valueAsNumber;
|
|
}}
|
|
min="0"
|
|
step="1"
|
|
type="number" />
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr class="synthetic">
|
|
<td>
|
|
Synthetic T3/T4 Combo
|
|
</td>
|
|
<td class="right">
|
|
<div>
|
|
<input
|
|
value={compounded.value.t3Syn}
|
|
onInput={(e) => {
|
|
t4Ratio.value = compounded.value.t4Syn;
|
|
t3Ratio.value = (e.currentTarget as HTMLInputElement).valueAsNumber;
|
|
numGrains.value = t3Ratio.value / MPG_T3_SYN + t4Ratio.value / MPG_T4_SYN;
|
|
}}
|
|
min="0"
|
|
step="1"
|
|
type="number" />
|
|
</div> : <div>
|
|
<input
|
|
value={compounded.value.t4Syn}
|
|
onInput={(e) => {
|
|
t3Ratio.value = compounded.value.t3Syn;
|
|
t4Ratio.value = (e.currentTarget as HTMLInputElement).valueAsNumber;
|
|
numGrains.value = t3Ratio.value / MPG_T3_SYN + t4Ratio.value / MPG_T4_SYN;
|
|
}}
|
|
min="0"
|
|
step="1"
|
|
type="number" />
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="text-slab">
|
|
<strong>Changelog:</strong>
|
|
<p>
|
|
<ul>
|
|
<li>
|
|
November 2024: Migrated to new web framework and fixed some buggy input.
|
|
</li>
|
|
<li>
|
|
13th March 2024: Removed the synthetic/pure distinction as it was confusing and unnecessary
|
|
bloat.
|
|
</li>
|
|
</ul>
|
|
</p>
|
|
</div>
|
|
</>;
|
|
}
|
|
});
|