nice
This commit is contained in:
298
app/generative-energy/calc.ts
Normal file
298
app/generative-energy/calc.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
import { css, DJElement, h, html, qs } from "./util";
|
||||
|
||||
//@ts-check
|
||||
const MPG_T3_SYN = 25;
|
||||
const MPG_T4_SYN = 100;
|
||||
|
||||
class GlobalEventBus {
|
||||
/**
|
||||
* @private
|
||||
* @type {{ onChange(): void }[]}
|
||||
*/
|
||||
changeSubscribers = [];
|
||||
|
||||
/**
|
||||
* @param {{ onChange(): void }} listener
|
||||
*/
|
||||
addListener(listener) {
|
||||
this.changeSubscribers.push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} sender
|
||||
*/
|
||||
sendChange(sender) {
|
||||
for (const sub of this.changeSubscribers) {
|
||||
if (sub === sender) continue;
|
||||
sub.onChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const eventBus = new GlobalEventBus();
|
||||
|
||||
class GrainInput {
|
||||
/** @type GrainInput[] */
|
||||
static inputs = [];
|
||||
/** @type string */
|
||||
name;
|
||||
/** @type string */
|
||||
unit;
|
||||
/** @type number */
|
||||
mpg;
|
||||
/** @type HTMLInputElement */
|
||||
inputEl;
|
||||
/** @type GrainInput */
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} insertionPoint
|
||||
*/
|
||||
attachInput(insertionPoint) {
|
||||
insertionPoint.appendChild(this.inputEl);
|
||||
this.inputEl.valueAsNumber = this.mpg;
|
||||
this.inputEl.addEventListener("input", (e) => {
|
||||
const newVal = /** @type HTMLInputElement | null */ (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 {
|
||||
/** @type HTMLInputElement */
|
||||
t3Ratio = h("input", { type: "number", min: 0, step: 1 });
|
||||
/** @type HTMLInputElement */
|
||||
t4Ratio = h("input", { type: "number", min: 0, step: 1 });
|
||||
/** @type HTMLInputElement */
|
||||
t3Syn = h("input", { type: "number", min: 0 });
|
||||
/** @type HTMLInputElement */
|
||||
t4Syn = h("input", { type: "number", min: 0 });
|
||||
/** @type number */
|
||||
t3 = 1;
|
||||
/** @type number */
|
||||
t4 = 4;
|
||||
/** @type GrainInput */
|
||||
reference;
|
||||
|
||||
/**
|
||||
* @param {GrainInput} referenceInput
|
||||
*/
|
||||
constructor(referenceInput) {
|
||||
this.reference = referenceInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, HTMLElement>} inputs
|
||||
*/
|
||||
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 = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0;
|
||||
this.t3 = newVal;
|
||||
this.onChange();
|
||||
});
|
||||
|
||||
this.t4Ratio.addEventListener("input", (e) => {
|
||||
const newVal = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0;
|
||||
this.t4 = newVal;
|
||||
this.onChange();
|
||||
});
|
||||
|
||||
this.t3Syn.addEventListener("input", (e) => {
|
||||
const newVal = /** @type HTMLInputElement | null */ (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 = /** @type HTMLInputElement | null */ (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`
|
||||
table {
|
||||
border-collapse: separate;
|
||||
margin: auto;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 5px;
|
||||
border-spacing: 0;
|
||||
border: 1px solid var(--text-color);
|
||||
}
|
||||
|
||||
input {
|
||||
width: 70px;
|
||||
}
|
||||
tr:last-of-type td {
|
||||
border-bottom: none;
|
||||
}
|
||||
td {
|
||||
border-bottom: 1px solid var(--text-color);
|
||||
border-right: 1px solid var(--text-color);
|
||||
padding: 10px;
|
||||
}
|
||||
td:last-of-type {
|
||||
border-right: none;
|
||||
}
|
||||
.breathe {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
@media (min-width: 600px) {
|
||||
td.right div {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
static template = html`
|
||||
<table>
|
||||
<tbody id="table-body">
|
||||
<tr id="compounded-start" class="ratios">
|
||||
<td>
|
||||
Desired Ratio (T3:T4)
|
||||
</td>
|
||||
<td class="right">
|
||||
<div><slot class="t3"></slot></div> :
|
||||
<div><slot class="t4"></slot></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="synthetic">
|
||||
<td>
|
||||
Synthetic T3/T4 Combo
|
||||
</td>
|
||||
<td class="right">
|
||||
<div><slot class="t3"></slot>mcg</div> :
|
||||
<div><slot class="t4"></slot>mcg</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`;
|
||||
|
||||
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),
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("thyroid-converter", ThyroidConverter);
|
||||
Reference in New Issue
Block a user