Source: ivFluid.js

/**
 * IV Fluid Module
 * @module ivFluid
 * @since v1.4.0
 */

const sumVolume = ( acc, cur ) => { return acc + cur.volume };

// const additives = [];
// const fluids = [];
// const macros = [];
// const lytes = [];
let qsFluid;

class IVFluidComponent {
  constructor(obj) {
    this.id = obj.id;
    this.unit = obj.unit;
    this.roundingFactor = obj.roundingFactor || 0.1;
    this.outputTotal = $(`#${obj.id}-total`);
    this.outputConc = $(`#${obj.id}-tConc`);
    
    if ( obj.mgPerUnit ) {
      this.outputPercent = $(`#${obj.id}-tPercent`);
      this.percentRoundingFactor = obj.percentRoundingFactor || 0.1;
      this.mgPerUnit = obj.mgPerUnit;
    }
    
  }
  get total() {
    const self = this;
    return everything.reduce((acc, cur) => { return acc + cur[self.id] }, 0);
  }
};
export default class IvFluid {
  constructor(obj) {
    const self = this;
    this.opts = {
      id: {
        pre:          obj.idPre,
        inputAmount:  obj.idInputAmount  || '-amount',
        outputVolume: obj.idOutputVolume || '-volume',
        qsCheckbox:   obj.idQsCheckbox   || '-qs',
        selectUnit:   obj.idSelectUnit   || '-units',
        outputConc:   obj.idOutputConc   || '-conc',
        outputAmount: obj.idOutputAmount || '-amt2',
      },
    };
    this.components = {
      dextrose: new IVFluidComponent({  opts: self.opts, id: 'dextrose',  unit: 'g',   mgPerUnit: 1000 }),
      sodium: new IVFluidComponent({    opts: self.opts, id: 'sodium',    unit: 'mEq', mgPerUnit: 58.44, percentRoundingFactor: 0.01 }),
      potassium: new IVFluidComponent({ opts: self.opts, id: 'potassium', unit: 'mEq'  }),
      phosphate: new IVFluidComponent({ opts: self.opts, id: 'phosphate', unit: 'mmol' }),
      calcium: new IVFluidComponent({   opts: self.opts, id: 'calcium',   unit: 'mEq'  }),
      magnesium: new IVFluidComponent({ opts: self.opts, id: 'magnesium', unit: 'g'    }),
      chloride: new IVFluidComponent({  opts: self.opts, id: 'chloride',  unit: 'mEq'  }),
      acetate: new IVFluidComponent({   opts: self.opts, id: 'acetate',   unit: 'mEq'  }),
      osmoles: new IVFluidComponent({   opts: self.opts, id: 'osmoles',   unit: 'mOsm' }),
      sw: new Fluid({    opts: self.opts, id: 'sw' }),
      d5w: new Fluid({   opts: self.opts, id: 'd5w',   osm: 0.252,    dextrose: 0.05 }),
      d10w: new Fluid({  opts: self.opts, id: 'd10w',  osm: 0.504,    dextrose: 0.1 }),
      d5ns: new Fluid({  opts: self.opts, id: 'd5ns',  osm: 0.56,    dextrose: 0.05,  sodium: 0.154, chloride: 0.154 }),
      d5hns: new Fluid({ opts: self.opts, id: 'd5hns', osm: 0.406,    dextrose: 0.05, sodium: 0.077, chloride: 0.077 }),
      d5qns: new Fluid({ opts: self.opts, id: 'd5qns', osm: 0.329,    dextrose: 0.05, sodium: 0.0385 }),
      ns: new Fluid({    opts: self.opts, id: 'ns',    osm: 0.308,    sodium: 0.154,  chloride: 0.154 }),
      hns: new Fluid({   opts: self.opts, id: 'hns',   osm: 0.154,    sodium: 0.077,  chloride: 0.077 }),
      lr: new Fluid({
        opts: self.opts,
        id: 'lr',
        osm: 0.273,
        sodium: 0.13,
        chloride: 0.109,
        acetate: 0.028,
        potassium: 0.004,
        calcium: 3 / 1000,
      }),
      d5lr: new Fluid({
        opts: self.opts,
        id: 'd5lr',
        osm: 525 / 1000,
        dextrose: 5 / 100,
        sodium: 130 / 1000,
        chloride: 109 / 1000,
        acetate: 28 / 1000,
        potassium: 4 / 1000,
        calcium: 2.7 / 1000,
      }),
      cacl: new Additive({
        opts: self.opts,
        id: 'cacl',
        name: 'calcium chloride',
        unit: ['mEq', 'g'],
        conc: [1.36, 0.1],
        osm: 2.04,
        calcium: 1.36,
        chloride: 1.36,
      }),
      cagluc: new Additive({
        opts: self.opts,
        id: 'cagluc',
        unit: ['mEq', 'g'],
        conc: [0.465, 0.1],
        osm: 0.68,
        calcium: 0.465,
      }),
      mg: new Additive({
        opts: self.opts,
        id: 'mg',
        unit: ['g', 'mEq'],
        conc: [0.5, 4.06],
        osm: 4.06,
        magnesium: 0.5,
      }),
      kacet: new Additive({
        id: 'kacet',
        unit: ['mEq'],
        conc: [2],
        osm: 4,
        potassium: 2,
        acetate: 2,
      }),
      kcl: new Additive({
        opts: self.opts,
        id: 'kcl',
        unit: ['mEq'],
        conc: [2],
        osm: 4,
        potassium: 2,
        chloride: 2,
      }),
      naacet: new Additive({
        opts: self.opts,
        id: 'naacet',
        unit: ['mEq'],
        conc: [2],
        osm: 4,
        sodium: 2,
        acetate: 2,
      }),
      nabicarb: new Additive({
        opts: self.opts,
        id: 'nabicarb',
        unit: ['mEq'],
        conc: [1],
        osm: 2,
        sodium: 1,
        acetate: 1,
      }),
      nacl: new Additive({
        id: 'nacl',
        unit: ['mEq'],
        conc: [4],
        osm: 8,
        sodium: 4,
        chloride: 4,
      }),
      kphos: new Additive({
        opts: self.opts,
        id: 'kphos',
        unit: ['mmol', 'mEq'],
        conc: [3, 4.4],
        osm: 7.4,
        potassium: 4.4,
        phosphate: 3,
      }),
      naphos: new Additive({
        opts: self.opts,
        id: 'naphos',
        unit: ['mmol', 'mEq'],
        conc: [3, 4],
        osm: 12,
        sodium: 4,
        phosphate: 3,
      }),
      mv: new Additive({
        opts: self.opts,
        id: 'mv',
        unit: ['dose'],
        conc: [0.1],
        osm: 4.11,
      }),
      thia: new Additive({
        opts: self.opts,
        id: 'thia',
        name: 'thiamine',
        unit: ['mg'],
        conc: [100],
        osm: 0.62,
      }),
      folic: new Additive({
        opts: self.opts,
        id: 'folic',
        unit: ['mg'],
        conc: [5],
        osm: 0.2,
      }),
      cyan: new Additive({
        opts: self.opts,
        id: 'cyan',
        name: 'cyanocobalamin',
        unit: ['mcg'],
        conc: [1000],
        osm: 0.45,
      }),
      d50w: new Additive({
        opts: self.opts,
        id: 'd50w',
        unit: ['g'],
        conc: [0.5],
        osm: 2.52,
        dextrose: 0.5,
      }),
      aa: new Macro({
        opts: self.opts,
        id: 'aa',
        unit: ['g', 'mL'],
        conc: [0.1, 0.15],
        osm: [0.95, 1.383],
        phosphate: [0.01, 0.015], //is this actually the same? for AA15?
        sodium: [0.01, 0.015], //is this actually the same? for AA15?
        acetate: [0.089, 0.151],
      }),
      dex: new Macro({
        opts: self.opts,
        id: 'dex',
        unit: ['g', 'mL'],
        conc: [0.2, 0.3, 0.5, 0.7],
        osm: [1.01, 1.514, 2.523, 3.53],
        dextrose: [0.2, 0.3, 0.5, 0.7],
      }),
    };
    this.lytes = [
      this.components.dextrose,
      this.components.sodium,
      this.components.potassium,
      this.components.phosphate,
      this.components.calcium,
      this.components.magnesium,
      this.components.chloride,
      this.components.acetate,
      this.components.osmoles,
    ];
    this.fluids = [
      this.components.sw,
      this.components.d5w,
      this.components.d10w,
      this.components.d5ns,
      this.components.d5hns,
      this.components.d5qns,
      this.components.ns,
      this.components.hns,
      this.components.lr,
      this.components.d5lr,
    ];
    this.additives = [
      this.components.cacl,
      this.components.cagluc,
      this.components.mg,
      this.components.kacet,
      this.components.kcl,
      this.components.naacet,
      this.components.nabicarb,
      this.components.nacl,
      this.components.kphos,
      this.components.naphos,
      this.components.mv,
      this.components.thia,
      this.components.folic,
      this.components.cyan,
      this.components.d50w,
    ];
    this.macros = [
      this.components.aa,
      this.components.dex,
    ];
  }
  
};

class Ingredient {
  constructor(obj) {
    this.id = obj.id;
    this.name = obj.name;
    this._unit = obj.unit || ['mL'];
    this._conc = obj.conc || [1];
    this.lytes = {
      na: obj.sodium || 0,
      k: obj.potassium || 0,
      ca: obj.calcium || 0,
      cl: obj.chloride || 0,
      mg: obj.magnesium || 0,
      phos: obj.phosphate || 0,
      acet: obj.acetate || 0,
      dex: obj.dextrose || 0,
      osm: obj.osm || 0,
    };
    this.inputAmount = $(`#${obj.opts.id.pre}${obj.id}${obj.opts.id.inputAmount}`);
    this.outputVolume = $(`#${obj.opts.id.pre}${obj.id}${obj.opts.id.outputVolume}`);
    this.volume = 0;
  }
  get conc() {
    if ( this._conc.length > 1 ) {
      return this._conc[this.selectUnit[0].selectedIndex];
    }
    return this._conc[0];
  }
  get unit() {
    if ( this._unit.length > 1 ) {
      return this._unit[this.selectUnit[0].selectedIndex];
    }
    return this._unit[0];
  }
  get unit2() {
    if (this._unit.length > 1 ) {
      return this._unit[Math.abs(this.selectUnit[0].selectedIndex - 1)];
    }
    return undefined;
  }
  get conc2() {
    if (this._unit.length > 1 ) {
      return this._conc[Math.abs(this.selectUnit[0].selectedIndex - 1)];
    }
    return undefined;
  }
  get sodium() { return this.lytes.na * this.volume}
  get potassium() { return this.lytes.k * this.volume}
  get calcium() { return this.lytes.ca * this.volume}
  get chloride() { return this.lytes.cl * this.volume}
  get magnesium() { return this.lytes.mg * this.volume}
  get phosphate() { return this.lytes.phos * this.volume}
  get acetate() { return this.lytes.acet * this.volume}
  get dextrose() { return this.lytes.dex * this.volume}
  get osmoles() { return this.lytes.osm * this.volume}
}
class Fluid extends Ingredient {
  constructor(obj) {
    super(obj);
    this.isQS = false;
    this.qsCheckbox = $(`#${obj.opts.id.pre}${obj.id}${obj.opts.id.qsCheckbox}`);
  }
}
class Additive extends Ingredient {
  constructor(obj) {
    super(obj);
    if (obj.unit.length > 1) {
      this.selectUnit =  $(`#${obj.opts.id.pre}${obj.id}${obj.opts.id.selectUnit}`);
      this.outputConc = $(`#${obj.opts.id.pre}${obj.id}${obj.opts.id.outputConc}`);
      this.outputAmount = $(`#${obj.opts.id.pre}${obj.id}${obj.opts.id.outputAmount}`);
    }
  }
}
class Macro extends Ingredient {
  constructor(obj) {
    super(obj);
    this.lytes = {
      na: obj.sodium || Array(obj.conc.length).fill(0),
      k: obj.potassium || Array(obj.conc.length).fill(0),
      ca: obj.calcium || Array(obj.conc.length).fill(0),
      cl: obj.chloride || Array(obj.conc.length).fill(0),
      mg: obj.magnesium || Array(obj.conc.length).fill(0),
      phos: obj.phosphate || Array(obj.conc.length).fill(0),
      acet: obj.acetate || Array(obj.conc.length).fill(0),
      dex: obj.dextrose || Array(obj.conc.length).fill(0),
      osm: obj.osm || Array(obj.conc.length).fill(0),
    };
    this.selectUnit = $(`#${obj.id}-units`);
    this.selectConc = $(`#${obj.id}-macroConc`);
  }
  get conc() {
    if ( this._conc.length > 1 ) {
      return this._conc[this.selectConc[0].selectedIndex];
    }
    return this._conc[0];
  }
  get sodium() { return this.lytes.na[this.selectConc[0].selectedIndex] * this.volume }
  get potassium() { return this.lytes.k[this.selectConc[0].selectedIndex] * this.volume }
  get calcium() { return this.lytes.ca[this.selectConc[0].selectedIndex] * this.volume }
  get chloride() { return this.lytes.cl[this.selectConc[0].selectedIndex] * this.volume }
  get magnesium() { return this.lytes.mg[this.selectConc[0].selectedIndex] * this.volume }
  get phosphate() { return this.lytes.phos[this.selectConc[0].selectedIndex] * this.volume }
  get acetate() { return this.lytes.acet[this.selectConc[0].selectedIndex] * this.volume }
  get dextrose() { return this.lytes.dex[this.selectConc[0].selectedIndex] * this.volume }
  get osmoles() { return this.lytes.osm[this.selectConc[0].selectedIndex] * this.volume }
}


const everything = macros.concat(fluids.concat(additives));

// when qs base is checked, uncheck others, setAmount with current and old (if applicable)
$('.check-qs').on('change', function(ev) {
  const fluidID = ev.target.id.split('-', 1)[0];
  const oldQsObj = fluids.find( ({ isQS }) => isQS === true );
  const nonQS = fluids.filter( ({ id }) => id !== fluidID);
  nonQS.forEach(fluid => { fluid.isQS = false });
  const fluidObj = fluids.find( ({ id }) => id === fluidID );
  if (ev.target.checked) {
    fluidObj.isQS = true;
    $(`.check-qs:not(#${ev.target.id})`).prop( 'checked', false );
  } else {
    fluidObj.isQS = false;
  }
  if (oldQsObj) {
    setAmount(oldQsObj.id);
  }
  setAmount(fluidID);
});

$('.select-macroConc').on('change', function(ev) {
  setAmount(ev.target.id.split('-', 1)[0]);
});

$('.input-amount').on('keyup', function(ev) {
  setAmount(ev.target.id.split('-', 1)[0]);
});

$('.select-unit').on('change', function(ev) {
  const additiveID = ev.target.id.split('-', 1)[0];
  const obj = additives.find( ({ id }) => id === additiveID);
  obj.outputConc.html(`${roundTo(obj.conc, 0.001)} ${obj.unit}/mL`);
  setAmount(additiveID);
});

function setAmount(curID) {
  const obj = everything.find( ({ id }) => id === curID );
  const amt = +$(obj.inputAmount).val();
  const vol = roundTo(amt / obj.conc, 0.00000000001);
  if ( obj.outputAmount ) {
    obj.outputAmount.html(vol > 0 ? `${roundTo(vol * obj.conc2, 0.001)} ${obj.unit2}` : '');
  }
  // calculate volume if this is the qs base
  if ( !obj.isQS ) {
    obj.volume = vol;
  } else {
    const others = everything.filter(({ id }) => id !== curID);
    const otherVolume = others.reduce(sumVolume, 0);
    obj.volume = otherVolume >= vol ? 0 : vol - otherVolume;
  }
  // output the volume
  obj.outputVolume.html(amt > 0 ? `${roundTo(obj.volume, 0.1)} mL` : '');
  
  // if there is a qs base, recalculate its amounts
  const qsBase = fluids.find( ({ isQS }) => isQS === true );
  if ( qsBase ) {
    if ( qsBase.id !== curID ) {
      setAmount(qsBase.id);
    }
  }
  getTotals();
}

function getTotals() {
  const totalVolume = everything.reduce(sumVolume, 0);
  $('#total-volume').html(totalVolume > 0 ? `${roundTo(totalVolume, 0.1)} mL` : '');
  lytes.forEach(lyte => {
    const amt = lyte.total;
    if ( amt > 0 ) {
      lyte.outputTotal.html(`${roundTo(amt, lyte.roundingFactor)} ${lyte.unit}`);
      lyte.outputConc.html(`${roundTo(amt / (totalVolume / 1000), 0.1)} ${lyte.unit}/L`);
      if (lyte.mgPerUnit) {
        const conc = amt / totalVolume * (lyte.mgPerUnit / 10);
        lyte.outputPercent.html(`${roundTo(conc, lyte.percentRoundingFactor)}%`);
      }
    } else {
      lyte.outputTotal.html('');
      lyte.outputConc.html('');
      if (lyte.mgPerUnit) {
        lyte.outputPercent.html('');
      }
    }
  });

  // move the osmolarity indicator and highlight the osmolarity category
  const finOsm = osmoles.total / (totalVolume / 1000);
  if ( finOsm > 0 ) {
    const pos = `${roundTo(  finOsm / 1000  * 100, 0.0001 )  }%`;
    $('.marker-current').css({ 'left': pos, 'display': 'block' });
    if (finOsm > 1000) {
      $('.marker-current').css({ 'transform': 'translateX(-5px) rotate(-45deg)' });
    } else {
      $('.marker-current').css({ 'transform': 'translateX(-5px) rotate(45deg)' });
    }
  } else {
    $('.marker-current').css({ 'display': 'none' });
  }
  $('.osm-flag').removeClass('active');
  let highlightClass = '.osm-flag--';
  highlightClass += finOsm > 900 ? 'hyper2' :
    finOsm > 500 ? 'hyper1' :
      finOsm > 280 ? 'iso'    :
        finOsm > 154 ? 'hypo2'  :
          finOsm > 0 ? 'hypo1' : '';
  if ( finOsm > 0 ) {
    $(highlightClass).addClass('active');
  }

  // Calcium-Phosphate Solubility
  const volLiters = totalVolume / 1000;
  const caAmt = cacl.volume * 1.36 + cagluc.volume * 0.47; //mEq
  const phosAmt1 = kphos.volume * 4.4 + naphos.volume * 4; //mEq
  const freamine = aa.volume * aa.conc * 0.13;
  const solubility1 = caAmt / volLiters * ( (phosAmt1 + freamine) / volLiters );
  const phosAmt2 = (kphos.volume + naphos.volume) * 3 + aa.phosphate * aa.volume; //mmol
  const solubility2 = (caAmt + phosAmt2) / volLiters;
  $('#solu-value-1').html(solubility1 > 0 ? roundTo(solubility1, 1) : '');
  $('#solu-value-2').html(solubility2 > 0 ? roundTo(solubility2, 1) : '');
  $('#solu-slider-1').val(solubility1 > 0 ? solubility1 : 0);
  $('#solu-slider-2').val(solubility2 > 0 ? solubility2 : 0);

  if (solubility1 === 0) {
    $('#solu-slider-2').val(0);
  }
  
  $('.solu-wrapper').removeClass('warning danger');
  // solu-wrapper-1, 2 addClass warning/danger
  
  if ( solubility1 >= 1200 ) {
    $('.solu-wrapper-1').addClass('danger');
  } else if ( solubility1 >= 300 ) {
    $('.solu-wrapper-1').addClass('warning');
  }
  
  if ( solubility2 >= 60 && solubility1 > 0 ) {
    $('.solu-wrapper-2').addClass('danger');
  } else if ( solubility2 >= 30 && solubility1 > 0) {
    $('.solu-wrapper-2').addClass('warning');
  }
}

function roundTo(x, n) {
  let t = Math.round(x / n) * n;
  t = Math.floor(t * 1000);
  t = t / 1000;
  return t;
}
/*

//-------------------------------------------------------------
function resetAll() {

  slideSolu1.value = 0;
  slideSolu2.value = 0;
  document.documentElement.style.setProperty('--slide-2-color', 'darkgreen');
  var ins = document.getElementsByTagName('input');
  var outs = document.getElementsByClassName('output');
  var combos = document.getElementsByTagName('select');
  currentMarker.style.display = "none";

  for(i=0;i<outs.length;i++){
    outs[i].innerHTML = "";
  }

  for(i=0;i<combos.length;i++){
    combos[i].selectedIndex = 0;
  }

  for(i=0;i<ins.length;i++){
    if(ins[i].type == "text") {
      ins[i].value = "";
    } else if (ins[i].type == "checkbox") {
      ins[i].checked = false;
    }
  }
  setColor('z');
  aa.inputAmount.focus();
}
*/