import {
  InputValuesTable
} from "./chunk-322QC6R4.js";
import {
  RegressionResults
} from "./chunk-B6CSYKZJ.js";
import {
  mayRunSnplstTask
} from "./chunk-7OYOC7XY.js";
import {
  PlotBase,
  fillTermWrapper,
  get$id,
  getCombinedTermFilter,
  make_one_checkbox,
  sayerror,
  termsettingInit
} from "./chunk-SA7APTJR.js";
import {
  Menu
} from "./chunk-HYOEWQ5P.js";
import {
  copyMerge,
  getCompInit
} from "./chunk-M3J4MINX.js";
import {
  get_bin_label
} from "./chunk-QH6W3NVF.js";
import {
  dictionaryNumericTypes,
  isDictionaryType,
  isNumericTerm,
  numericTypes
} from "./chunk-NOEAT6CX.js";
import {
  select_default
} from "./chunk-I6Y4O3RR.js";

// plots/regression.inputs.term.js
var InputTerm = class {
  constructor(opts) {
    this.opts = opts;
    this.section = opts.section;
    this.term = opts.term;
    this.parent = opts.parent;
    this.vocabApi = this.parent.parent.vocabApi || this.parent.app.vocabApi;
  }
  async init(holder) {
    const termRow = holder.append("div");
    const pillDiv = termRow.append("span").style("display", "inline-block");
    const interactionDiv = termRow.append("span").style("margin-left", "20px");
    this.dom = {
      holder,
      termRow,
      pillDiv,
      interactionDiv,
      err_div: holder.append("div").style("display", "none").style("padding", "5px").style("background-color", "rgba(255,100,100,0.2)"),
      infoDiv: holder.append("div"),
      tip: new Menu()
    };
    try {
      const { app, config, state, disable_terms } = this.parent;
      const arg = {
        placeholder: this.section.selectPrompt,
        placeholderIcon: this.section.placeholderIcon,
        holder: this.dom.pillDiv,
        vocabApi: this.vocabApi,
        noTermPromptOptions: this.opts.noTermPromptOptions,
        activeCohort: state.activeCohort,
        debug: app.opts.debug,
        menuOptions: this.section.configKey == "outcome" ? "{edit,reuse,replace}" : "{edit,reuse,remove}",
        usecase: { target: "regression", detail: this.section.configKey, regressionType: config.regressionType },
        disable_terms,
        abbrCutoff: 50,
        genomeObj: this.parent.parent.genomeObj,
        // required for snplocus
        defaultQ4fillTW: get_defaultQ4fillTW(config.regressionType, this.section.configKey),
        callback: (term) => {
          this.parent.editConfig(this, term);
        }
      };
      this.furbishTsConstructorArg(arg);
      this.pill = await termsettingInit(arg);
      if (this.section.configKey == "outcome") {
        this.setQ = getQSetter4outcome(config.regressionType);
      }
      this.valuesTable = new InputValuesTable({
        holder: this.dom.infoDiv,
        input: this,
        callback: (term) => {
          this.parent.editConfig(this, term);
        }
      });
    } catch (e) {
      this.displayError([e]);
    }
  }
  furbishTsConstructorArg(arg) {
    const type = this.parent.config.regressionType;
    if (this.section.configKey == "outcome") {
      if (type == "logistic") {
        arg.numericEditMenuVersion = ["binary"];
        return;
      }
      if (type == "linear") {
        arg.numericEditMenuVersion = ["continuous"];
        return;
      }
      if (type == "cox") {
        return;
      }
      throw "unknown regressionType";
    }
    if (this.section.configKey == "independent") {
      arg.numericEditMenuVersion = ["continuous", "discrete", "spline"];
      arg.geneVariantEditMenuOnlyGrp = true;
      return;
    }
    throw "unknown section.configKey: " + this.section.configKey;
  }
  displayError(errors) {
    this.hasError = true;
    this.dom.err_div.selectAll("*").remove();
    this.dom.err_div.style("display", "block").selectAll("div").data(Array.isArray(errors) ? errors : [errors]).enter().append("div").text((e) => e);
    this.parent.handleError();
    console.error(errors);
  }
  async main() {
    const tw = this.term;
    if (tw) {
      delete tw.error;
    }
    this.dom.err_div.style("display", "none").text("");
    this.hasError = false;
    const errors = [];
    try {
      if (tw && this.setQ) {
        const { app, state } = this.parent;
        await this.setQ[tw.term.type](tw, this.vocabApi, this.parent.parent.filter, state);
      }
      try {
        await this.updateTerm();
      } catch (e2) {
        errors.push(e2);
      }
      await this.pill.main(this.getPillArgs());
      this.renderInteractionPrompt();
      await this.valuesTable.main();
      if (tw && !tw.$id) tw.$id = await get$id(this.vocabApi.getTwMinCopy(tw));
      const e = tw && tw.error || this.pill.error;
      if (e) errors.push(e);
      if (errors.length) throw errors;
    } catch (errors2) {
      this.displayError(errors2);
    }
  }
  async updateTerm() {
    const tw = this.term;
    if (!tw) return;
    if (!tw.q) throw ".term.q missing on this input";
    if (!tw.q.mode && isDictionaryType(tw.term.type)) {
      if (tw.term.type == "categorical" || tw.term.type == "condition" || tw.term.type == "survival" || tw.term.type == "samplelst")
        tw.q.mode = "discrete";
      else tw.q.mode = "continuous";
    }
    const body = tw.term.type == "snplst" || tw.term.type == "snplocus" ? { cacheid: tw.q.cacheid } : { term1_q: tw.q };
    const wait = this.dom.holder.append("div").style("padding", "5px").text("Loading...");
    let data;
    try {
      data = await this.vocabApi.getCategories(tw.term, this.parent.parent.filter, body);
      if (!data) throw `no data for term.id='${tw.term.id}'`;
      if (data.error) throw data.error;
    } finally {
      wait.remove();
    }
    mayRunSnplstTask(tw, data);
    this.termStatus = {
      topInfoStatus: [],
      bottomSummaryStatus: void 0,
      sampleCounts: void 0,
      excludeCounts: void 0,
      allowToSelectRefGrp: false
    };
    if (tw.q.numOfSampleWithAnyValidGT) {
      const invalid_snps_count = tw.term.snps.reduce((i, j) => i + (j.invalid ? 1 : 0), 0);
      this.termStatus.topInfoStatus.push(
        `${tw.q.numOfSampleWithAnyValidGT} samples with valid genotypes` + (invalid_snps_count > 0 ? ` ${invalid_snps_count} invalid SNP${invalid_snps_count > 1 ? "s" : ""}.` : "")
      );
    }
    if ("geneticModel" in tw.q) {
      this.termStatus.topInfoStatus.push(
        "Genetic model: " + (tw.q.geneticModel == 0 ? "Additive" : tw.q.geneticModel == 1 ? "Dominant" : tw.q.geneticModel == 2 ? "Recessive" : "By genotype")
      );
    }
    if (tw.q.restrictAncestry) {
      this.termStatus.topInfoStatus.push("Analyzing " + tw.q.restrictAncestry.name);
      if (tw.q.restrictAncestry.PCcount) {
        this.termStatus.topInfoStatus.push(
          `Adjusting for top ${tw.q.restrictAncestry.PCcount} ancestry principal components`
        );
      }
    }
    if (tw.term.reachedVariantLimit) {
      this.termStatus.topInfoStatus.push(
        `<span class=sja_mcdot style="background:#aaa;font-size:1em">
				&nbsp;&#9888; Restricted to first ${tw.term.snps.length}
				variants of this region.&nbsp;</span> Try zooming in.`
      );
    }
    this.orderedLabels = data.orderedLabels;
    if (data.lst) {
      this.summarizeSample(tw, data.lst);
      if (isNumericTerm(tw.term)) {
        if (tw.q.mode != "continuous" && tw.q.mode != "spline") {
          this.termStatus.allowToSelectRefGrp = true;
        }
        if (tw.q.scale && tw.q.scale != 1) this.termStatus.topInfoStatus.push(`Scale: Per ${tw.q.scale}`);
        if (tw.q.mode == "discrete") {
          this.termStatus.topInfoStatus.push(`Discrete variable with ${this.termStatus.sampleCounts.length} bins`);
        }
        if (tw.q.mode == "spline") {
          this.termStatus.topInfoStatus.push(
            `Cubic spline variable with ${tw.q.knots.length} knots: ${tw.q.knots.map((x) => Number(x.value)).sort((a, b) => a - b).join(", ")}`
          );
        }
      } else if (tw.term.type == "categorical" || tw.term.type == "geneVariant" || tw.term.type == "samplelst") {
        this.termStatus.allowToSelectRefGrp = true;
      } else if (tw.term.type == "condition") {
        if (this.section.configKey == "outcome" && this.parent.opts.regressionType == "logistic") {
          this.termStatus.allowToSelectRefGrp = true;
        }
        if (this.section.configKey == "outcome" && this.parent.opts.regressionType == "cox") {
          if (!["age", "time"].includes(tw.q.timeScale)) throw "invalid q.timeScale";
          const tdb = this.vocabApi.termdbConfig;
          this.termStatus.topInfoStatus.push(`Time axis: ${tw.q.timeScale == "time" ? tdb.timeUnit : "age"}`);
          this.termStatus.topInfoStatus.push(
            `<span style="padding-left: 10px;">-start: ${tw.q.timeScale == "time" ? " " : "age at "}entry into the cohort (i.e., ${tdb.cohortStartTimeMsg})</span>`
          );
          this.termStatus.topInfoStatus.push(
            `<span style="padding-left: 10px;">-end: ${tw.q.timeScale == "time" ? " " : "age at "}event or censoring/death</span>`
          );
          const grades = Object.keys(tw.term.values).map(Number);
          const maxgrade = Math.max(...grades);
          this.termStatus.topInfoStatus.push(
            `<div style="padding-top: 8px;">Event: first occurrence of grade ${tw.q.breaks[0] === maxgrade ? tw.q.breaks[0] : `${tw.q.breaks[0]}-${maxgrade}</div>`}`
          );
        }
      }
      this.maySet_refGrp(tw);
      if (this.section.configKey == "outcome") {
        if (this.parent.config.regressionType == "logistic") {
          tw.nonRefGrp = getLogisticOutcomeNonref(tw);
        } else if (this.parent.config.regressionType == "cox") {
          tw.eventLabel = getCoxOutcomeEventLabel(tw);
        }
      }
    }
  }
  summarizeSample(tw, datalst) {
    const excluded_values = /* @__PURE__ */ new Set();
    if (tw.term.values) {
      for (const i in tw.term.values) {
        if (tw.term.values[i].uncomputable) excluded_values.add(tw.term.values[i].label);
      }
    }
    if (tw.q.mode == "cox") {
      const toExclude = datalst.find((x) => x.key == -1);
      if (toExclude) excluded_values.add(toExclude.label);
    }
    const sampleCounts = this.termStatus.sampleCounts = datalst.filter((v) => !excluded_values.has(v.label));
    const excludeCounts = this.termStatus.excludeCounts = datalst.filter((v) => excluded_values.has(v.label));
    const totalCount = { included: 0, excluded: 0, total: 0 };
    sampleCounts.forEach((v) => totalCount.included += v.samplecount);
    excludeCounts.forEach((v) => totalCount.excluded += v.samplecount);
    totalCount.total = totalCount.included + totalCount.excluded;
    if (tw.term.type == "condition" && totalCount.total) {
      totalCount.excluded = totalCount.total - totalCount.included;
    }
    this.termStatus.bottomSummaryStatus = `${totalCount.included} samples included` + (totalCount.excluded ? `. ${totalCount.excluded} samples excluded:` : "");
    if (tw && tw.q.mode !== "continuous" && sampleCounts.length < 2)
      throw `there should be two or more discrete values with samples for variable='${tw.term.name}'`;
  }
  maySet_refGrp(tw) {
    if (this.section.configKey == "outcome" && this.parent.config.regressionType == "cox") {
      return;
    }
    if (tw.q.mode == "continuous") {
      tw.refGrp = "NA";
      return;
    }
    const sc = this.termStatus.sampleCounts;
    if (!("refGrp" in tw) || !sc.find((i) => i.key == tw.refGrp)) {
      const o = this.orderedLabels;
      if (o && o.length) sc.sort((a, b) => o.indexOf(a.key) - o.indexOf(b.key));
      else sc.sort((a, b) => a.samplecount < b.samplecount ? 1 : -1);
      tw.refGrp = sc[0].key;
    }
  }
  getPillArgs() {
    const section = this.section;
    const { config, state, disable_terms } = this.parent;
    const args = Object.assign(
      {
        activeCohort: state.activeCohort,
        disable_terms,
        usecase: {
          target: "regression",
          detail: section.configKey,
          regressionType: config.regressionType
        }
      },
      this.term
    );
    args.filter = this.parent.parent.filter;
    return args;
  }
  remove() {
    this.dom.termRow.transition().duration(500).style("opacity", 0).remove();
    for (const key in this.dom) {
      delete this.dom[key];
    }
  }
  renderInteractionPrompt() {
    this.dom.interactionDiv.style("display", "none");
    if (!this.term) return;
    if (this.section.configKey != "independent") return;
    if (this.term.q.mode == "spline") return;
    if (this.vocabApi.termdbConfig.regression?.settings?.disableInteractions) return;
    {
      let count = 0;
      for (const input of this.section.inputLst) {
        if (input.term && input.term.q.mode != "spline") {
          count++;
        }
      }
      if (count < 2) return;
    }
    const n = this.term.interactions.length;
    this.dom.interactionDiv.style("display", "inline").html(n == 0 ? "Add interactions" : `${n} interaction${n > 1 ? "s" : ""}`).style("padding", "5px").style("background-color", n == 0 ? null : "#ececec").style("border-radius", n == 0 ? null : "6px").style("color", n == 0 ? "rgb(153, 153, 153)" : "#000").style("font-size", n == 0 ? "0.8em" : "").style("cursor", "pointer").on("click", () => this.renderInteractionOptions());
  }
  renderInteractionOptions() {
    const self = this;
    self.dom.tip.clear().showunder(self.dom.interactionDiv.node());
    if (self.parent.config.includeUnivariate) {
      const label = self.parent.dom.univariateCheckboxDiv.select("label").text().trim();
      self.dom.tip.d.append("div").text(`Cannot add interactions. Please uncheck the "${label}" checkbox.`);
      return;
    }
    self.dom.tip.d.append("div").style("padding", "5px").style("font-size", "0.8em").style("color", "rgb(153, 153, 153)").html(`Selected variables will each form pairwise interaction with ${this.term.term.name}`);
    self.dom.tip.d.append("div").selectAll("div").data(self.parent.config.independent.filter((tw) => tw && tw.$id != self.term.$id && tw.q.mode != "spline")).enter().append("div").style("margin", "5px").each(function(tw) {
      const elem = select_default(this).append("label");
      const checkbox = elem.append("input").attr("type", "checkbox").property("checked", self.term.interactions.includes(tw.$id));
      elem.append("span").text(" " + tw.term.name);
    });
    self.dom.tip.d.append("button").text("Apply").style("margin", "5px").on("click", () => {
      self.dom.tip.hide();
      self.term.interactions = [];
      self.dom.tip.d.selectAll("input").each(function(tw) {
        if (select_default(this).property("checked")) self.term.interactions.push(tw.$id);
      });
      for (const tw of self.parent.config.independent) {
        const interactions = new Set(tw.interactions);
        self.term.interactions.includes(tw.$id) ? interactions.add(self.term.$id) : interactions.delete(self.term.$id);
        tw.interactions = [...interactions];
      }
      self.parent.editConfig(self, self.term);
    });
  }
};
function getQSetter4outcome(regressionType) {
  return {
    integer: regressionType == "logistic" ? maySetTwoBins : setContMode,
    float: regressionType == "logistic" ? maySetTwoBins : setContMode,
    geneExpression: regressionType == "logistic" ? maySetTwoBins : setContMode,
    //Added geneExpression, but still breaks, need to fix
    categorical: maySetTwoGroups,
    condition: setQ4tteOutcome,
    survival: setQ4tteOutcome
  };
}
async function maySetTwoBins(tw, vocabApi, filter, state) {
  if (tw.q.mode == "binary" && tw.q.lst && tw.q.lst.length == 2) {
    tw.q.mode = "binary";
    return;
  }
  const data = await vocabApi.getPercentile(tw.term, [50], state.termfilter);
  if (data.error || !data.values.length || !Number.isFinite(data.values[0]))
    throw "cannot get median value: " + (data.error || "no data");
  const median = tw.term.type == "integer" ? Math.round(data.values[0]) : Number(data.values[0].toFixed(2));
  tw.q = {
    mode: "binary",
    type: "custom-bin",
    lst: [
      {
        startunbounded: true,
        stopinclusive: true,
        stop: median
      },
      {
        stopunbounded: true,
        startinclusive: false,
        start: median
      }
    ]
  };
  tw.q.lst.forEach((bin) => {
    bin.label = get_bin_label(bin, tw.q);
  });
  tw.refGrp = tw.q.lst[0].label;
}
function setQ4tteOutcome(tw, vocabApi, filter, state) {
  if (state.config.regressionType == "logistic") {
    if (!tw.refGrp) tw.refGrp = tw.q.groups[0].name;
  }
  if (state.config.regressionType == "cox") {
    if (!tw.q.timeScale) tw.q.timeScale = "time";
  }
}
async function maySetTwoGroups(tw, vocabApi, filter, state) {
  const { term, q } = tw;
  if (q.mode == "binary") {
    if (q.type == "values" && Object.keys(term.values).length == 2) return;
    if (q.type == "predefined-groupset") {
      const idx = q.predefined_groupset_idx;
      const t_gs2 = term.groupsetting;
      if (t_gs2[idx] && Object.keys(t_gs2[idx]).length == 2) return;
    }
    if (q.type == "custom-groupset") {
      const gs = q.customset;
      if (gs.groups.filter((g) => !g.uncomputable).length == 2) return;
    }
  }
  const data = await vocabApi.getCategories(term, filter);
  if (data.error) throw "cannot get categories: " + data.error;
  const category2samplecount = /* @__PURE__ */ new Map();
  const computableCategories = [];
  const uncomputableCategories = [];
  for (const i of data.lst) {
    category2samplecount.set(i.key, i.samplecount);
    if (term.values && term.values[i.key] && term.values[i.key].uncomputable) uncomputableCategories.push(i.key);
    else computableCategories.push(i.key);
  }
  if (computableCategories.length < 2) {
    q.type = "values";
    tw.error = "less than 2 categories/grades - cannot create separate groups";
    return;
  }
  if (computableCategories.length == 2) {
    q.type = "values";
    return;
  }
  const t_gs = term.groupsetting;
  if (q.customset && q.customset.groups && q.customset.groups.length == 2 && groupsetNoEmptyGroup(q.customset, category2samplecount)) {
    q.type = "custom-groupset";
    return;
  }
  if (t_gs && t_gs.lst) {
    if (q.predefined_groupset_idx >= 0 && t_gs.lst[q.predefined_groupset_idx] && t_gs.lst[q.predefined_groupset_idx].groups.length == 2 && groupsetNoEmptyGroup(t_gs.lst[q.predefined_groupset_idx], category2samplecount)) {
      q.type = "predefined-groupset";
      return;
    }
    const i = t_gs.lst.findIndex((g) => g.groups.length == 2);
    if (i != -1 && groupsetNoEmptyGroup(t_gs.lst[i], category2samplecount)) {
      q.predefined_groupset_idx = i;
      q.type = "predefined-groupset";
      return;
    }
  }
  const customset = {
    activeCohort: state.activeCohort,
    // creating 3 groups instead of 2 groups since current groupset UI expects first group to be excluded group
    // TODO: refactor client/termsetting/handlers/qualitative.ts to not consider the first group (i.e. group.currentIdx === 0) as the excluded group, but rather to consider group.excluded=true as the excluded group
    groups: [
      {
        name: "Excluded categories",
        type: "values",
        values: uncomputableCategories.map((v) => {
          return { key: v };
        })
      },
      {
        name: "Group 1",
        type: "values",
        values: []
      },
      {
        name: "Group 2",
        type: "values",
        values: []
      }
    ]
  };
  const group_i_cutoff = Math.round(computableCategories.length / 2);
  for (const [i, v] of computableCategories.entries()) {
    if (i < group_i_cutoff) customset.groups[1].values.push({ key: v });
    else customset.groups[2].values.push({ key: v });
  }
  q.customset = customset;
  q.type = "custom-groupset";
}
function setContMode(tw) {
  tw.q.mode = "continuous";
}
function groupsetNoEmptyGroup(gs, c2s) {
  for (const g of gs.groups) {
    let total = 0;
    if (g.type == "values") {
      for (const i of g.values) total += c2s.get(i.key) || 0;
      if (total == 0) return false;
    }
  }
  return true;
}
function getLogisticOutcomeNonref(outcome) {
  if (outcome.term.type == "condition") {
    for (const i of outcome.q.groups) {
      if (i.name != outcome.refGrp) return i.name;
    }
    throw "nonref group not found for logistic outcome";
  }
  if (outcome.q.type == "predefined-groupset") {
    if (!Number.isInteger(outcome.q.predefined_groupset_idx))
      throw 'outcome.q.predefined_groupset_idx not integer when q.type is "predefined-groupset"';
    if (!outcome.term.groupsetting) throw "outcome.term.groupsetting missing";
    const grpset = outcome.term.groupsetting.lst[outcome.q.predefined_groupset_idx];
    if (!grpset) throw "groupset not found by outcome.q.predefined_groupset_idx";
    const nonrefgrp = grpset.groups.find((i) => i.name != outcome.refGrp);
    if (!nonrefgrp) throw "non-ref group not found for predefined-groupset";
    return nonrefgrp.name;
  }
  if (outcome.q.type == "custom-groupset") {
    if (!outcome.q.customset) throw "outcome.q.customset missing";
    const nonrefgrp = outcome.q.customset.groups.find((i) => i.name != outcome.refGrp);
    if (!nonrefgrp) throw "non-ref group not found for custom-groupset";
    return nonrefgrp.name;
  }
  if (outcome.q.type == "values") {
    if (!outcome.term.values) throw "outcome.term.values{} missing";
    for (const k in outcome.term.values) {
      const v = outcome.term.values[k];
      if (v.label != outcome.refGrp) return v.label;
    }
    throw "unknown nonref group from outcome.term.values";
  }
  if (outcome.q.type == "custom-bin") {
    const nonrefbin = outcome.q.lst.find((i) => i.label != outcome.refGrp);
    if (!nonrefbin) throw "non-ref bin is not found for custom-bin";
    return nonrefbin.label;
  }
  if (outcome.q.type == "regular-bin") {
    throw "do not know a way to find computed bin list for type=regular-bin";
  }
  throw "unknown outcome.q.type";
}
function getCoxOutcomeEventLabel(tw) {
  let eventLabel;
  if (!tw.term.values) throw "tw.term.values missing";
  if (tw.term.type == "condition") {
    const grades = Object.keys(tw.term.values).map((k) => {
      const grade = Number(k);
      if (!Number.isFinite(grade)) throw "grade is not a number";
      return grade;
    });
    const startGrade = tw.q.breaks[0];
    const endGrade = Math.max(...grades);
    eventLabel = `Grades ${startGrade}-${endGrade}`;
  } else if (tw.term.type == "survival") {
    const exitCodes = Object.keys(tw.term.values).map((k) => {
      const code = Number(k);
      if (!Number.isFinite(code)) throw "exit code is not a number";
      return code;
    });
    eventLabel = tw.term.values[Math.max(...exitCodes)].label;
  } else {
    throw "unexpected tw.term.type";
  }
  return eventLabel;
}

// plots/regression.inputs.js
var allNonDictionaryTerms = [
  {
    termtype: "snplst",
    //html: 'A list of SNPs <span style="font-size:.7em">All SNPs are analyzed in one model</span>'
    html: "A list of variants"
  },
  {
    termtype: "snplocus",
    //html: 'Variants in a locus <span style="font-size:.7em">Variants are analyzed individually</span>'
    html: "Variants in a locus"
  },
  {
    termtype: "prs",
    text: "Polygenic risk score"
  }
];
var RegressionInputs = class {
  constructor(opts) {
    this.opts = opts;
    this.app = opts.app;
    this.parent = opts.parent;
    setInteractivity(this);
    setRenderers(this);
    this.createSectionConfigs();
    this.initUI();
  }
  createSectionConfigs() {
    this.outcome = {
      /*** static configuration ***/
      heading: "Outcome variable",
      selectPrompt: this.opts.regressionType == "linear" ? "Select continuous outcome variable" : "Select outcome variable",
      placeholderIcon: "",
      configKey: "outcome",
      limit: 1,
      usecase: { target: "regression", regressionType: this.opts.regressionType, detail: "outcome" },
      /*** dynamic configuration ***/
      inputLst: [],
      /*** tracker for this section's DOM elements ***/
      dom: {}
    };
    this.independent = {
      /*** static configuration ***/
      heading: "Independent variable(s)",
      selectPrompt: "Add independent variable",
      placeholderIcon: "",
      configKey: "independent",
      limit: 10,
      usecase: { target: "regression", regressionType: this.opts.regressionType, detail: "independent" },
      /*** dynamic configuration ***/
      inputLst: [],
      /*** tracker for this section's DOM elements ***/
      dom: {}
    };
    this.sections = [this.outcome, this.independent];
  }
  async main() {
    try {
      this.config = this.parent.config;
      this.state = this.parent.state;
      this.hasError = false;
      this.setDisableTerms();
      const updates = [];
      for (const section of this.sections) {
        await this.renderSection(section);
        for (const input of section.inputLst) {
          input.dom.holder.style("border-left", input.term ? "1px solid #bbb" : "");
          updates.push(input.main());
        }
      }
      await Promise.all(updates);
      for (const section of this.sections) {
        for (const input of section.inputLst) {
          if (input.term && input.term.error || input.hasError) {
            this.hasError = true;
          }
        }
      }
    } catch (e) {
      this.hasError = true;
      throw e;
    }
  }
  setDisableTerms() {
    this.disable_terms = [];
    if (this.config.outcome && this.config.outcome.term) this.disable_terms.push(this.config.outcome.term);
    if (this.config.independent) {
      for (const vb of this.config.independent) {
        this.disable_terms.push(vb.term);
      }
    }
  }
  handleError() {
    this.hasError = true;
    this.dom.submitBtn.property("disabled", true);
  }
  getNoTermPromptOptions(section) {
    if (section.configKey != "independent") return;
    const lst = [];
    for (const item of structuredClone(allNonDictionaryTerms)) {
      if (!this.state.allowedTermTypes.includes(item.termtype)) {
        continue;
      }
      if (section.inputLst.find((i) => i.term && i.term.term.type == item.termtype)) {
        continue;
      }
      if (item.termtype == "snplocus") {
        if (this.config.includeUnivariate) {
          item.invalid = true;
          const label = this.dom.univariateCheckboxDiv.select("label").text().trim();
          item.invalidMsg = `Cannot add this variable. Please uncheck the "${label}" checkbox.`;
        }
      }
      lst.push(item);
    }
    if (lst.length) {
      lst.unshift({
        isDictionary: true,
        text: "Dictionary variable"
      });
    }
    return lst;
  }
};
function setRenderers(self) {
  self.initUI = () => {
    const controls = self.opts.holder.append("div").style("display", "block");
    self.dom = {
      div: self.opts.holder,
      //.style('margin', '10px 0px'),
      controls,
      body: controls.append("div"),
      foot: controls.append("div").style("margin", "0px 20px").style("display", "flex").style("align-items", "center").style("gap", "20px")
    };
    self.dom.submitBtn = self.dom.foot.append("div").append("button").style("display", "none").style("padding", "5px 15px").style("border-radius", "15px").style("cursor", "pointer").text("Run analysis").on("click", self.submit);
    self.dom.univariateCheckboxDiv = self.dom.foot.append("div").style("display", "none");
    self.dom.univariateCheckbox = make_one_checkbox({
      labeltext: "Include univariate results",
      checked: false,
      holder: self.dom.univariateCheckboxDiv,
      callback: (checked) => {
        const invalid = self.config.independent.find((v) => v.interactions.length || v.term.type == "snplocus");
        if (invalid) {
          self.dom.univariateCheckbox.property("checked", false);
          const tip = new Menu();
          tip.showunder(self.dom.univariateCheckboxDiv.node());
          const msg = `Cannot include univariate results. Please remove ${invalid.interactions.length ? "all interactions" : `the "${invalid.term.name}" variable`}.`;
          tip.d.append("div").text(msg);
          return;
        }
        self.app.dispatch({
          type: "plot_edit",
          id: self.parent.id,
          chartType: "regression",
          config: {
            hasUnsubmittedEdits: true,
            includeUnivariate: checked
          }
        });
      }
    });
    self.dom.submitMsgs = self.dom.foot.append("div").style("color", "#cc0000").style("font-style", "italic").style("font-size", "0.8em");
    self.submitMsgs = {};
    for (const section of self.sections) {
      const div = self.dom.body.append("div");
      self.addSection(section, div);
    }
  };
  self.addSection = function(section, div) {
    div.style("display", "none").style("margin", "3px 5px").style("padding", "3px 5px");
    section.dom = {
      holder: div,
      headingDiv: div.append("div").style("margin", "3px 5px 20px 5px").style("font-size", "17px").style("color", "#bbb").text(section.heading),
      inputsDiv: div.append("div")
    };
  };
  self.renderSection = function(section) {
    section.dom.holder.style("display", section.configKey == "outcome" || self.config.outcome ? "block" : "none");
    syncInputsWithConfig(section);
    const inputs = section.dom.inputsDiv.selectAll(":scope > div").data(section.inputLst, (input) => input.term && (input.term.term.id || input.term.term.name));
    inputs.exit().each(removeInput);
    inputs.enter().append("div").each(addInput);
  };
  function syncInputsWithConfig(section) {
    const selected = self.config[section.configKey];
    const selectedArray = Array.isArray(selected) ? selected : selected ? [selected] : [];
    mayConvertInteractionIds(selectedArray, section);
    for (const variable of selectedArray) {
      if (section.configKey == "independent") {
        if (!variable.interactions) variable.interactions = [];
        for (const id of variable.interactions) {
          const tw = selectedArray.find((i) => i.$id == id);
          if (!tw) throw "interacting partner not found in independents: " + id;
          if (!tw.interactions) tw.interactions = [];
          if (!tw.interactions.includes(variable.$id)) tw.interactions.push(variable.$id);
        }
      }
      const input = section.inputLst.find((input2) => input2.term?.$id == variable.$id);
      if (!input) {
        section.inputLst.push(
          new InputTerm({
            section,
            term: variable,
            parent: self
          })
        );
      } else {
        input.term = variable;
      }
    }
    mayAddBlankInput(section, self);
  }
  function mayConvertInteractionIds(selectedArray, section) {
    if (section.configKey != "independent") return;
    const id2$id = /* @__PURE__ */ new Map();
    for (const v of selectedArray) {
      if (v.id) id2$id.set(v.id, v.$id);
    }
    for (const v of selectedArray) {
      if (v.interactions?.length) {
        for (const [i, id] of v.interactions.entries()) {
          const $id = id2$id.get(id);
          if ($id) v.interactions[i] = $id;
        }
      }
    }
  }
  async function addInput(input) {
    await input.init(
      select_default(this).style("width", "fit-content").style("margin", "0px 15px 35px 25px").style("padding", "0px 5px")
    );
  }
  function removeInput(input) {
    input.remove();
    for (const key in input.dom) {
      delete input.dom[key];
    }
    const div = select_default(this);
    div.transition().duration(500).style("opacity", 0).remove();
  }
  self.resetSubmitButton = () => {
    self.dom.submitBtn.text("Run analysis").style("display", self.config.outcome && self.config.independent.length ? "block" : "none").property("disabled", self.hasError);
  };
  self.mayShowUnivariateCheckbox = () => {
    if (!self.config.outcome || self.config.independent.length < 2) {
      self.dom.univariateCheckboxDiv.style("display", "none");
      return;
    }
    self.dom.univariateCheckboxDiv.style("display", "block");
    self.dom.univariateCheckbox.property("checked", self.config.includeUnivariate);
  };
  self.mayShowSubmitMsgs = () => {
    self.dom.submitMsgs.selectAll("div").data(Object.values(self.submitMsgs)).join("div").text((d) => d);
  };
}
function setInteractivity(self) {
  self.editConfig = async (input, variable) => {
    if (!variable) {
      const i = input.section.inputLst.findIndex((d) => d === input);
      if (i == -1) throw `deleting an unknown input`;
      input.section.inputLst.splice(i, 1);
      if (input.term) {
        for (const other of input.section.inputLst) {
          if (!other.term || !other.term.interactions?.length) continue;
          const i2 = other.term.interactions.indexOf(input.term.$id);
          if (i2 != -1) other.term.interactions.splice(i2, 1);
        }
      }
    } else {
      const prevTerm = input.term;
      if (prevTerm && variable.term.id === prevTerm.term.id) {
        for (const k in prevTerm) {
          if (!(k in variable)) variable[k] = prevTerm[k];
        }
      }
      input.term = variable;
      if (variable.q.mode == "spline" && variable.interactions) {
        for (const other of input.section.inputLst) {
          if (!other.term || !other.term.interactions) continue;
          const i = other.term.interactions.indexOf(input.term.$id);
          if (i != -1) other.term.interactions.splice(i, 1);
        }
        variable.interactions = [];
      }
    }
    const selected = [];
    for (const i of input.section.inputLst) {
      if (i.term) selected.push(i.term);
    }
    const key = input.section.configKey;
    const configValue = Array.isArray(self.config[key]) ? selected : selected[0];
    self.app.dispatch({
      type: "plot_edit",
      id: self.parent.id,
      chartType: "regression",
      config: {
        hasUnsubmittedEdits: true,
        // replace config.outcome or config.independent
        [key]: JSON.parse(JSON.stringify(configValue))
      }
    });
  };
  self.submit = () => {
    self.dom.submitBtn.property("disabled", true);
    if (self.hasError) {
      alert("Please fix the input variable errors (highlighted in red background).");
      return;
    }
    const config = JSON.parse(JSON.stringify(self.config));
    config.hasUnsubmittedEdits = false;
    self.app.dispatch({
      type: "plot_edit",
      id: self.parent.id,
      chartType: "regression",
      config
    });
  };
}
function mayAddBlankInput(section, self) {
  if (section.inputLst.length >= section.limit) {
    return;
  }
  const blankInput = section.inputLst.find((i) => !i.term);
  if (blankInput) {
    const noTermPromptOptions = self.getNoTermPromptOptions(section);
    if (noTermPromptOptions) {
      blankInput.pill.main({ noTermPromptOptions });
    }
    return;
  }
  section.inputLst.push(
    new InputTerm({
      section,
      parent: self,
      noTermPromptOptions: self.getNoTermPromptOptions(section)
    })
  );
}

// plots/regression.js
var Regression = class _Regression extends PlotBase {
  static type = "regression";
  constructor(opts) {
    super(opts);
    this.type = _Regression.type;
    this.genomeObj = opts.app.opts.genome;
  }
  async init(appState) {
    this.dom = {
      header: this.opts.header,
      // header is optional
      errordiv: this.opts.holder.append("div"),
      inputs: this.opts.holder.append("div").style("margin", "20px 10px"),
      results: this.opts.holder.append("div").style("margin-left", "40px")
    };
    const config = appState.plots.find((p) => p.id === this.id);
    this.inputs = new RegressionInputs({
      app: this.app,
      parent: this,
      id: this.id,
      holder: this.dom.inputs,
      regressionType: config.regressionType
    });
    this.results = new RegressionResults({
      app: this.app,
      parent: this,
      id: this.id,
      holder: this.dom.results,
      regressionType: config.regressionType
    });
  }
  getState(appState, sub) {
    const config = appState.plots.find((p) => p.id === this.id);
    if (!config) {
      throw `No plot with id='${this.id}' found. Did you set this.id before this.api = getComponentApi(this)?`;
    }
    if (!config.regressionType) throw "regressionType is required";
    const parentConfig = this.parentId && appState.plots.find((p) => p.id === this.parentId);
    const termfilter = getCombinedTermFilter(appState, config.filter || parentConfig?.filter);
    return {
      vocab: appState.vocab,
      formIsComplete: config.outcome && config.independent.length,
      activeCohort: appState.activeCohort,
      termfilter,
      config,
      allowedTermTypes: appState.termdbConfig.allowedTermTypes,
      minTimeSinceDx: appState.termdbConfig.minTimeSinceDx
    };
  }
  /* do not set reactsTo
  so it reacts to all actions matching with the plot id (controlled by store method)
  including filter/cohort change
  */
  async main() {
    try {
      this.config = JSON.parse(JSON.stringify(this.state.config));
      this.mayUpdateSandboxHeader();
      this.getFilter();
      await this.inputs.main();
      await this.results.main();
      this.inputs.resetSubmitButton();
      this.inputs.mayShowUnivariateCheckbox();
      this.inputs.mayShowSubmitMsgs();
    } catch (e) {
      if (this.inputs.hasError) {
        this.results.main();
      }
      sayerror(this.dom.errordiv, "Error: " + (e.error || e));
      if (e.stack) console.log(e.stack);
    }
  }
  mayUpdateSandboxHeader() {
    if (!this.dom.header) return;
    const o = this.config.outcome;
    this.dom.header.html(
      (o ? o.term.name : "") + '<span style="opacity:.6;font-size:.7em;margin-left:10px;">' + this.config.regressionType.toUpperCase() + " REGRESSION</span>"
    );
  }
  getFilter() {
    const filters = [];
    if (this.state.termfilter?.filter) filters.push(this.state.termfilter.filter);
    const tws = [this.config.outcome, ...this.config.independent];
    const tws_restrictAncestry = tws.filter((tw) => tw?.q.restrictAncestry);
    if (tws_restrictAncestry.length) {
      if (tws_restrictAncestry.length > 1) {
        const ancestries = new Set(tws_restrictAncestry.map((tw2) => tw2.q.restrictAncestry.name));
        if (ancestries.size > 1) throw "samples cannot be restricted to more than 1 ancestry";
      }
      const tw = tws_restrictAncestry[0];
      filters.push({ type: "tvs", tvs: tw.q.restrictAncestry.tvs });
      this.inputs.submitMsgs.restrictAncestry = `Restricting analysis to samples of ${tw.q.restrictAncestry.name}`;
    } else {
      delete this.inputs.submitMsgs.restrictAncestry;
    }
    this.filter = { type: "tvslst", join: "and", lst: filters };
  }
};
var regressionInit = getCompInit(Regression);
var componentInit = regressionInit;
var _ID_ = 1;
async function getPlotConfig(opts, app) {
  if (!opts.outcome) opts.outcome = {};
  await fillTermWrapper(opts.outcome, app.vocabApi, get_defaultQ4fillTW(opts.regressionType, "outcome"));
  const id = "id" in opts ? opts.id : `_REGRESSION_${_ID_++}`;
  const config = { id };
  config.outcome = opts.outcome;
  if (opts.independent) {
    if (!Array.isArray(opts.independent)) throw ".independent[] is not array";
    for (const t of opts.independent) {
      await fillTermWrapper(
        t,
        app.vocabApi,
        t.q?.mode ? void 0 : get_defaultQ4fillTW(opts.regressionType, "independent")
      );
    }
    config.independent = opts.independent;
  } else {
    config.independent = [];
  }
  return copyMerge(config, opts);
}
function get_defaultQ4fillTW(regressionType, useCase = "") {
  const defaultQ = {};
  defaultQ["numeric"] = regressionType == "logistic" && useCase == "outcome" ? { mode: "binary" } : { mode: "discrete" };
  for (const t of numericTypes) {
    if (dictionaryNumericTypes.has(t)) continue;
    defaultQ[t] = regressionType == "logistic" && useCase == "outcome" ? { mode: "binary" } : { mode: "discrete", type: "custom-bin", preferredBins: "median" };
  }
  defaultQ["categorical"] = regressionType == "logistic" && useCase == "outcome" ? { mode: "binary" } : { mode: "discrete" };
  if (useCase == "outcome") {
    if (regressionType == "cox") {
      defaultQ.condition = { mode: "cox" };
    }
    if (regressionType == "logistic") {
      defaultQ.condition = { mode: "binary" };
    }
  }
  defaultQ["geneVariant"] = { type: "predefined-groupset" };
  return defaultQ;
}
function makeChartBtnMenu(holder, chartsInstance) {
  const allMethods = [
    { label: "Linear", type: "linear" },
    { label: "Logistic", type: "logistic" },
    { label: "Cox", type: "cox" }
  ];
  const useMethods = allMethods.filter((i) => chartsInstance.state.currentCohortChartTypes.includes(i.type));
  if (useMethods.length == 0) return holder.append("div").text("Error: no methods available");
  if (useMethods.length == 1) {
    chartsInstance.dom.tip.hide();
    chartsInstance.prepPlot({
      config: {
        chartType: "regression",
        regressionType: useMethods[0].type,
        independent: []
      }
    });
    return;
  }
  for (const { label, type } of useMethods) {
    holder.append("div").attr("class", "sja_menuoption sja_sharp_border").text(label).on("click", () => {
      chartsInstance.dom.tip.hide();
      chartsInstance.prepPlot({
        config: {
          chartType: "regression",
          regressionType: type,
          independent: []
        }
      });
    });
  }
}

export {
  regressionInit,
  componentInit,
  getPlotConfig,
  get_defaultQ4fillTW,
  makeChartBtnMenu,
  InputTerm,
  RegressionInputs
};
//# sourceMappingURL=chunk-VOUMS6IP.js.map
