import {
  VolcanoModel,
  getGroupColors
} from "./chunk-UOVALHAJ.js";
import {
  DataPointInteractions,
  GeneSetEditUI,
  MultiTermWrapperEditUI,
  PlotBase,
  axisstyle,
  controlsInit,
  downloadTable,
  fillTermWrapper,
  getCombinedTermFilter,
  getDefaultVolcanoSettings,
  getGEunit,
  getSampleNum,
  renderTable,
  sayerror,
  table2col,
  to_svg,
  validateVolcanoSettings
} from "./chunk-SA7APTJR.js";
import "./chunk-HJ6L54YS.js";
import "./chunk-LSEFWW72.js";
import "./chunk-SKREEF3H.js";
import {
  Menu
} from "./chunk-HYOEWQ5P.js";
import "./chunk-HBW42TDT.js";
import "./chunk-G6O3URDN.js";
import "./chunk-FN5XPUPH.js";
import "./chunk-IIT367QZ.js";
import "./chunk-RZGEKL77.js";
import "./chunk-UI7OCM2A.js";
import "./chunk-YZK37YCC.js";
import "./chunk-7EMWHCVW.js";
import "./chunk-7IYJZZQI.js";
import {
  copyMerge,
  getCompInit
} from "./chunk-M3J4MINX.js";
import "./chunk-PF4DSFDR.js";
import "./chunk-QH6W3NVF.js";
import {
  DNA_METHYLATION,
  GENE_EXPRESSION,
  PROTEOME_DAP,
  SINGLECELL_CELLTYPE
} from "./chunk-NOEAT6CX.js";
import "./chunk-4UWS5Y3N.js";
import "./chunk-DD4R5P6W.js";
import "./chunk-JNITUVXP.js";
import "./chunk-KSGA62R2.js";
import {
  axisBottom,
  axisLeft
} from "./chunk-LOZEKOES.js";
import "./chunk-TOU7EVFQ.js";
import {
  linear
} from "./chunk-OAWQ6LOO.js";
import {
  roundValueAuto
} from "./chunk-TLT4YIG3.js";
import "./chunk-KYBIQBXE.js";
import {
  selectAll_default,
  select_default
} from "./chunk-I6Y4O3RR.js";
import {
  rgb
} from "./chunk-OMR2DT66.js";
import "./chunk-DQC5FFGV.js";
import "./chunk-HFNDKYVF.js";

// plots/volcano/viewModel/VolcanoViewModel.ts
var VolcanoViewModel = class {
  constructor(config, response, settings) {
    this.numSignificant = 0;
    this.numNonSignificant = 0;
    this.minLogFoldChange = 0;
    this.maxLogFoldChange = 0;
    //Used for the y axis domain
    this.minLogPValue = 0;
    this.maxLogPValue = 0;
    //Unpadded extents — used for the visible axis labels/ticks (only span real data)
    this.minLogFoldChangeAxis = 0;
    this.maxLogFoldChangeAxis = 0;
    this.minLogPValueAxis = 0;
    this.maxLogPValueAxis = 0;
    //Dot radius in pixels (from server) — overlay rings size to match the PNG
    this.dotRadiusPx = 2;
    //Used in place of 0 p values that cannot be log transformed
    this.minNonZeroPValue = 1e-9;
    this.offset = 10;
    this.bottomPad = 60;
    this.horizPad = 70;
    this.topPad = 40;
    this.config = config;
    this.response = response;
    this.plotX = this.horizPad + this.offset * 2;
    this.dataRows = response.data.dots;
    const { caseColor, controlColor } = getGroupColors(this.config);
    const barplot = { colorNegative: controlColor, colorPositive: caseColor };
    this.pValueTable = {
      columns: [
        { label: "log\u2082(fold-change)", barplot, sortable: true },
        { label: "Original p-value", sortable: true },
        { label: "Adjusted p-value", sortable: true }
      ],
      /** Arr set in setPointData() if settings.showPValueTable is true to
       * prevent unnecessary data processing when the table is not shown */
      rows: [],
      height: settings.height + this.topPad
    };
    this.settings = settings;
    this.termType = config.termType;
    this.dataType = this.setDataType();
    this.setMinMaxValues();
    const plotDim = this.setPlotDimensions();
    this.setPTableColumns();
    const pointData = this.setPointData(plotDim, controlColor, caseColor);
    if (this.settings.showPValueTable) {
      const foldChangeIdx = this.pValueTable.columns.findIndex((c) => c.label.includes("log\u2082(fold-change)"));
      this.pValueTable.rows.sort((a, b) => b[foldChangeIdx].value - a[foldChangeIdx].value);
    }
    this.viewData = {
      images: response.images || [],
      termInfo: this.setTermInfo(plotDim),
      plotDim,
      pointData,
      pValueTableData: this.pValueTable,
      statsData: this.setStatsData(),
      userActions: this.setUserActions(),
      volcanoPng: response.data.volcanoPng,
      plotExtent: response.data.plotExtent
    };
  }
  setDataType() {
    if (this.termType == GENE_EXPRESSION) return "genes";
    if (this.termType == DNA_METHYLATION) return "promoters";
    if (this.termType == SINGLECELL_CELLTYPE) return "genes";
    if (this.termType == PROTEOME_DAP) return "proteins";
    throw new Error(`Unknown termType: ${this.termType}`);
  }
  setMinMaxValues() {
    const ext = this.response.data.plotExtent;
    this.minLogFoldChange = ext.xMin;
    this.maxLogFoldChange = ext.xMax;
    this.minLogPValue = ext.yMin;
    this.maxLogPValue = ext.yMax;
    this.minLogFoldChangeAxis = ext.xMinUnpadded;
    this.maxLogFoldChangeAxis = ext.xMaxUnpadded;
    this.minLogPValueAxis = ext.yMinUnpadded;
    this.maxLogPValueAxis = ext.yMaxUnpadded;
    this.dotRadiusPx = ext.dotRadiusPx;
    if (ext.minNonZeroPValue > 0) this.minNonZeroPValue = ext.minNonZeroPValue;
  }
  setPlotDimensions() {
    const ext = this.response.data.plotExtent;
    const plotW = ext.pixelWidth;
    const plotH = ext.pixelHeight;
    const xPlotScale = linear().domain([this.minLogFoldChange, this.maxLogFoldChange]).range([0, plotW]);
    const yPlotScale = linear().domain([this.minLogPValue, this.maxLogPValue]).range([plotH, 0]);
    const xScale = linear().domain([this.minLogFoldChangeAxis, this.maxLogFoldChangeAxis]).range([xPlotScale(this.minLogFoldChangeAxis), xPlotScale(this.maxLogFoldChangeAxis)]);
    const yScale = linear().domain([this.minLogPValueAxis, this.maxLogPValueAxis]).range([yPlotScale(this.minLogPValueAxis), yPlotScale(this.maxLogPValueAxis)]);
    return {
      svg: {
        //20 is for the term info above the plot
        height: plotH + this.topPad + this.bottomPad * 2 + this.offset * 3,
        width: plotW + this.horizPad * 2
      },
      top: {
        x: this.plotX,
        y: 5
      },
      xAxisLabel: {
        x: this.horizPad + plotW / 2 + this.offset,
        y: this.topPad + plotH + this.bottomPad + this.offset
      },
      xScale: {
        scale: xScale,
        x: this.plotX,
        y: plotH + this.topPad + this.offset * 2
      },
      yAxisLabel: {
        text: `-log10(${this.settings.pValueType} P value)`,
        x: this.horizPad / 3,
        y: this.topPad + plotH / 2
      },
      yScale: {
        scale: yScale,
        x: this.horizPad,
        y: this.topPad
      },
      plot: {
        height: plotH,
        width: plotW,
        x: this.plotX,
        y: this.topPad
      },
      logFoldChangeLine: {
        x: xPlotScale(0) + this.plotX,
        y1: this.topPad,
        y2: plotH + this.offset * 4
      },
      xPlotScale,
      yPlotScale
    };
  }
  setTermInfo(plotDim) {
    if (this.termType != GENE_EXPRESSION && this.termType != DNA_METHYLATION && this.termType != PROTEOME_DAP && this.termType != SINGLECELL_CELLTYPE)
      return;
    const getLabel = (name) => {
      if (name.length >= 25) return name.substring(0, 20) + "...";
      return name;
    };
    if (this.termType == PROTEOME_DAP) {
      return {
        y: plotDim.top.y + 10,
        first: {
          label: getLabel(`Control (${this.response.sample_size1})`),
          x: 0
        },
        second: {
          label: getLabel(`Case (${this.response.sample_size2})`),
          x: this.settings.width
        }
      };
    }
    if (this.termType == SINGLECELL_CELLTYPE) {
      const groupLabel = `${this.config.termId} ${this.config.categoryName}`;
      return {
        y: plotDim.top.y + 10,
        first: {
          label: getLabel(`Not in ${groupLabel}`),
          x: 0
        },
        second: {
          label: getLabel(groupLabel),
          x: this.settings.width
        }
      };
    }
    return {
      //Set slightly above the plot
      y: plotDim.top.y + 10,
      first: {
        // color: controlColor || this.settings.defaultSignColor,
        label: getLabel(`${this.config.samplelst.groups[0].name} (${this.response.sample_size1})`),
        x: 0
        // rectX: this.settings.width/2 - 10,
      },
      second: {
        // color: caseColor || this.settings.defaultSignColor,
        label: getLabel(`${this.config.samplelst.groups[1].name} (${this.response.sample_size2})`),
        x: this.settings.width
        // rectX: this.settings.width/2 + 10,
      }
    };
  }
  setPointData(_plotDim, controlColor, caseColor) {
    const radius = this.dotRadiusPx;
    const dataCopy = structuredClone(this.dataRows);
    for (const d of dataCopy) {
      const highlightKey = this.termType === DNA_METHYLATION ? d.promoter_id : d.gene_name;
      d.highlighted = this.config?.highlightedData?.includes(highlightKey);
      d.significant = true;
      this.getGenesColor(d, d.significant, controlColor, caseColor);
      if (d.significant) {
        this.numSignificant++;
        const row = [
          { value: roundValueAuto(d.fold_change) },
          { value: roundValueAuto(d.original_p_value) },
          { value: d.adjusted_p_value != void 0 ? roundValueAuto(d.adjusted_p_value) : "" }
        ];
        if (this.termType == DNA_METHYLATION) {
          row.splice(0, 0, { value: d.promoter_id || "" }, { value: d.gene_name || "" });
        } else if (this.termType == PROTEOME_DAP) {
          row.splice(0, 0, { value: d.gene_name || "" }, { value: d.gene || "" });
        } else {
          row.splice(0, 0, { value: d.gene_name || "" });
        }
        if (this.settings.showPValueTable) this.pValueTable.rows.push(row);
      } else {
        this.numNonSignificant++;
      }
      d.x = d.pixel_x + this.plotX;
      d.y = d.pixel_y + this.topPad;
      d.radius = radius;
    }
    this.numSignificant = this.response.data.totalSignificantRows;
    this.numNonSignificant = Math.max(0, this.response.data.totalRows - this.numSignificant);
    dataCopy.sort((a, b) => a.highlighted - b.highlighted);
    return dataCopy;
  }
  getGenesColor(d, significant, controlColor, caseColor) {
    if (!d.gene_name && this.termType != DNA_METHYLATION)
      throw new Error(`Missing gene_name in data: ${JSON.stringify(d)}`);
    if (significant) {
      if (controlColor && caseColor) d.color = d.fold_change > 0 ? caseColor : controlColor;
      else d.color = this.settings.defaultSignColor;
    } else d.color = this.settings.defaultNonSignColor;
  }
  setStatsData() {
    const tableRows = [
      {
        label: `Percentage of significant ${this.dataType}`,
        value: roundValueAuto(this.numSignificant * 100 / (this.numSignificant + this.numNonSignificant))
      },
      {
        label: `Number of significant ${this.dataType}`,
        value: this.numSignificant
      },
      {
        label: `Number of total ${this.dataType}`,
        value: this.numSignificant + this.numNonSignificant
      }
    ];
    if (this.termType == GENE_EXPRESSION || this.termType == DNA_METHYLATION) {
      tableRows.push(
        {
          label: this.config.samplelst.groups[0].name + " sample size (control group)",
          value: this.response.sample_size1
        },
        {
          label: this.config.samplelst.groups[1].name + " sample size (case group)",
          value: this.response.sample_size2
        }
      );
    } else if (this.termType == PROTEOME_DAP) {
      tableRows.push(
        {
          label: "Control sample size",
          value: this.response.sample_size1
        },
        {
          label: "Case sample size",
          value: this.response.sample_size2
        }
      );
    }
    if (this.response.bcv !== void 0 && this.response.bcv !== null) {
      tableRows.push({
        label: "Biological coefficient of variation",
        value: roundValueAuto(this.response.bcv)
      });
    }
    return tableRows;
  }
  setPTableColumns() {
    if (this.termType == DNA_METHYLATION) {
      this.pValueTable.columns.splice(0, 0, { label: "Promoter", sortable: true }, { label: "Gene(s)", sortable: true });
    } else if (this.termType == PROTEOME_DAP) {
      this.pValueTable.columns.splice(0, 0, { label: "Identifier", sortable: true }, { label: "Gene", sortable: true });
    } else {
      this.pValueTable.columns.splice(0, 0, { label: "Gene Name", sortable: true });
    }
  }
  setUserActions() {
    const userActions = {
      noShow: /* @__PURE__ */ new Set()
    };
    if (this.termType == GENE_EXPRESSION) {
      if (this.settings.method == "edgeR" && getSampleNum(this.config) > 100) {
        userActions.noShow.add("Confounding factors");
      }
      if (this.settings.method == "wilcoxon") userActions.noShow.add("Confounding factors");
    }
    return userActions;
  }
};

// plots/volcano/interactions/VolcanoInteractions.ts
var VolcanoInteractions = class {
  constructor(app, id, dom) {
    this.app = app;
    this.dom = dom;
    this.id = id;
    this.pValueTableData = [];
    this.data = [];
  }
  /** Launches a multi-term select tree
   * On submit, dispatches a plot_edit action with the new confounders */
  async confoundersMenu() {
    const state = this.app.getState();
    const config = state.plots.find((p) => p.id === this.id);
    if (config.termType !== GENE_EXPRESSION && config.termType !== DNA_METHYLATION) return;
    const allowedGroupNames = /* @__PURE__ */ new Set([config.samplelst.groups[0].name, config.samplelst.groups[1].name]);
    const grpTerms = new Set(
      (this.app?.vocabApi?.state.groups || []).filter((g) => allowedGroupNames.has(g.name)).flatMap(
        (g) => g.filter.lst.flatMap((f) => {
          if (f.tvs?.term) return f.tvs.term;
          else return f.lst.map((l) => l.tvs.term);
        })
      )
    );
    const disable_terms = grpTerms.size ? Array.from(grpTerms) : [];
    const maxNum = config.settings.volcano.method == "edgeR" ? 1 : 2;
    const ui = new MultiTermWrapperEditUI({
      app: this.app,
      callback: async (tws) => {
        this.dom.actionsTip.hide();
        await this.app.dispatch({
          type: "plot_edit",
          id: this.id,
          config: { confounderTws: tws }
        });
      },
      holder: this.dom.actionsTip.d,
      headerText: "Select confounders",
      maxNum,
      state,
      twList: config.confounderTws,
      disable_terms
    });
    await ui.renderUI();
  }
  download(termType) {
    this.dom.actionsTip.clear().showunder(this.dom.controls.select("div").node());
    const opts = [
      {
        text: "Download plot",
        callback: () => {
          const svg = this.dom.holder.select("svg").node();
          to_svg(svg, `Differential ${termType} analysis volcano`, { apply_dom_styles: true });
        }
      },
      {
        text: "Download p value table",
        callback: () => {
          downloadTable(this.pValueTableData.rows, this.pValueTableData.columns);
        }
      }
    ];
    for (const opt of opts) {
      this.dom.actionsTip.d.append("div").attr("class", "sja_menuoption").text(opt.text).on("click", opt.callback);
    }
  }
  async highlightDataPoint(value) {
    const config = this.app.getState().plots.find((p) => p.id === this.id);
    const highlightedData = config.highlightedData.includes(value) ? config.highlightedData.filter((d) => d !== value) : [...config.highlightedData, value];
    await this.app.dispatch({
      type: "plot_edit",
      id: this.id,
      config: { highlightedData }
    });
  }
  /** When clicking on a data point, launches the box plot in a separate sandbox
   * For geneExpression, value == gene symbol */
  launchBoxPlot(value) {
    const config = this.app.getState().plots.find((p) => p.id === this.id);
    const values = {};
    for (const group of config.samplelst.groups) {
      values[group.name] = {
        key: group.name,
        label: group.name,
        list: group.values
      };
    }
    const setTerm = () => {
      if (config.termType == GENE_EXPRESSION) {
        return {
          q: { mode: "continuous" },
          term: {
            gene: value,
            name: value,
            type: config.termType
          }
        };
      } else return config.term;
    };
    this.app.dispatch({
      type: "plot_create",
      config: {
        chartType: "summary",
        childType: "boxplot",
        term: setTerm(),
        term2: {
          q: { groups: config.tw.q.groups, type: "custom-samplelst" },
          term: config.tw.term
        }
      }
    });
  }
  /** Launch a violin plot for a gene expression data point. */
  launchViolinGeneExp(value) {
    const config = this.app.getState().plots.find((p) => p.id === this.id);
    this.app.dispatch({
      type: "plot_create",
      config: {
        chartType: "summary",
        childType: "violin",
        term: {
          q: { mode: "continuous" },
          term: {
            gene: value,
            name: value,
            type: config.termType
          }
        },
        term2: {
          q: { groups: config.tw.q.groups, type: "custom-samplelst" },
          term: config.tw.term
        }
      }
    });
  }
  launchGeneSetEdit() {
    const plotConfig = this.app.getState().plots.find((p) => p.id === this.id);
    const holder = this.dom.actionsTip.d.append("div").style("padding", "5px");
    const limitedGenesList = plotConfig.termType === DNA_METHYLATION ? this.data.map((d) => d.promoter_id) : this.data.map((d) => d.gene_name);
    new GeneSetEditUI({
      holder,
      genome: this.app.opts.genome,
      vocabApi: this.app.vocabApi,
      limitedGenesList,
      geneList: plotConfig.highlightedData.map((d) => {
        return { gene: d };
      }),
      customInputs: [
        {
          label: "Cancel highlight",
          getDisplayStyle: () => plotConfig.highlightedData.length > 0 ? "" : "none",
          showInput: async () => {
            await this.app.dispatch({
              type: "plot_edit",
              id: this.id,
              config: { highlightedData: [] }
            });
            this.dom.actionsTip.hide();
          }
        }
      ],
      callback: async (result) => {
        const highlightedData = result.geneList.map((d) => d.gene);
        await this.app.dispatch({
          type: "plot_edit",
          id: this.id,
          config: { highlightedData }
        });
        this.dom.actionsTip.hide();
      }
    });
  }
  /** When clicking on a DM data point, dispatches a DMR plot that runs DMRCate
   * analysis and renders a genome browser Block with DMR regions on their own
   * track. */
  async launchDmr(d) {
    const config = this.app.getState().plots.find((p) => p.id === this.id);
    const controlColor = config?.tw?.term?.values?.[config?.samplelst?.groups[0].name]?.color || "#ff0000";
    const caseColor = config?.tw?.term?.values?.[config?.samplelst?.groups[1].name]?.color || "#0000ff";
    const label = d.promoterId || `${d.chr}:${d.start}-${d.stop}`;
    const dmrConfig = {
      chartType: "dmr",
      headerText: `DMR: ${label}`,
      coordinateOverride: { chr: d.chr, start: d.start, stop: d.stop },
      group1: config.samplelst.groups[0].values || [],
      group2: config.samplelst.groups[1].values || [],
      group1Name: config.samplelst.groups[0].name,
      group2Name: config.samplelst.groups[1].name,
      settings: {
        colors: { group1: controlColor, group2: caseColor }
      }
    };
    this.app.dispatch({
      type: "plot_create",
      config: dmrConfig
    });
  }
  /** Launch a violin/box plot for a DNA methylation promoter.
   * Creates a methylation term using the promoter's chr/start/stop coordinates.
   * The tw handler fills in id and unit from termdbConfig. */
  launchDNAMethViolin(d) {
    const config = this.app.getState().plots.find((p) => p.id === this.id);
    const genomicFeatureType = d.promoter_id ? "promoter" : "gene";
    const featureName = genomicFeatureType === "gene" ? d.gene_name?.split(",")[0]?.trim() || "" : "";
    this.app.dispatch({
      type: "plot_create",
      config: {
        chartType: "summary",
        childType: "violin",
        term: {
          q: { mode: "continuous" },
          term: {
            genomicFeatureType,
            featureName,
            type: DNA_METHYLATION,
            chr: d.chr,
            start: d.start,
            stop: d.stop
          }
        },
        term2: {
          q: { groups: config.tw.q.groups, type: "custom-samplelst" },
          term: config.tw.term
        }
      }
    });
  }
  async launchDEGClustering() {
    const geneIndex = this.pValueTableData.columns.findIndex((col) => col.label === "Gene Name");
    const adjustedPValIndex = this.pValueTableData.columns.findIndex((col) => col.label === "Adjusted p-value");
    const rowsSorted = [...this.pValueTableData.rows].sort((a, b) => {
      const aQVal = Number(a[adjustedPValIndex].value);
      const bQVal = Number(b[adjustedPValIndex].value);
      return aQVal - bQVal;
    });
    const geneList = rowsSorted.slice(0, 100).map((r) => ({ gene: r[geneIndex].value }));
    const tws = geneList.map((d) => {
      const gene = d.gene;
      const unit = getGEunit(this.app.vocabApi);
      const name = `${gene} ${unit}`;
      const term = { gene, name, type: GENE_EXPRESSION };
      return { term, q: {} };
    });
    const group = { lst: tws, type: "hierCluster" };
    const customVariable = this.app.getState().plots.find((p) => p.id === this.id).tw;
    const annotationGroup = { lst: [customVariable] };
    const config = {
      chartType: "hierCluster",
      termgroups: [group, annotationGroup],
      dataType: GENE_EXPRESSION,
      filter: {
        in: true,
        join: "",
        type: "tvslst",
        lst: [{ type: "tvs", tvs: { term: customVariable.term } }]
      }
    };
    await this.app.dispatch({
      type: "plot_create",
      config: structuredClone(config)
    });
  }
};

// plots/volcano/view/VolcanoPlotView.ts
var VolcanoPlotView = class {
  constructor(dom, interactions, termType) {
    this.dom = dom;
    this.interactions = interactions;
    this.termType = termType;
    const actions = this.dom.holder.append("div").attr("id", "sjpp-volcano-actions").style("display", "block").style("z-index", 1).style("position", "relative");
    const svg = this.dom.holder.append("svg").style("display", "inline-block").attr("id", "sjpp-volcano-svg").style("vertical-align", "top");
    this.volcanoDom = {
      actions,
      svg,
      pValueTable: void 0,
      top: void 0,
      xAxis: void 0,
      xAxisLabel: void 0,
      yAxis: void 0,
      yAxisLabel: void 0,
      plot: void 0
    };
  }
  render(settings, viewData) {
    this.settings = settings;
    this.viewData = viewData;
    const plotDim = this.viewData.plotDim;
    this.initDom();
    this.renderUserActions();
    this.renderPlot(plotDim);
    renderDataPoints(this);
    this.renderFoldChangeLine(plotDim);
    this.attachInteractions(plotDim);
    if (this.settings.showPValueTable) this.renderPValueTable();
  }
  initDom() {
    this.volcanoDom.actions.selectAll("*").remove();
    this.volcanoDom.svg.selectAll("*").remove();
    const svg = this.volcanoDom.svg;
    this.volcanoDom.top = svg.append("g").attr("id", "sjpp-volcano-top");
    this.volcanoDom.xAxis = svg.append("g").attr("id", "sjpp-volcano-xAxis");
    this.volcanoDom.yAxis = svg.append("g").attr("id", "sjpp-volcano-yAxis");
    this.volcanoDom.xAxisLabel = svg.append("text").attr("id", "sjpp-volcano-xAxisLabel").attr("text-anchor", "middle");
    this.volcanoDom.yAxisLabel = svg.append("text").attr("id", "sjpp-volcano-yAxisLabel").attr("text-anchor", "middle");
    this.volcanoDom.plot = svg.append("g").attr("id", "sjpp-volcano-plot");
    this.dom.holder.select("#sjpp-volcano-pValueTable").remove();
    if (!this.settings.showPValueTable) return;
    this.volcanoDom.pValueTable = this.dom.holder.append("div").attr("id", "sjpp-volcano-pValueTable").attr("data-testid", "sjpp-volcano-pValueTable").style("display", "inline-block").style("vertical-align", "top");
  }
  renderUserActions() {
    this.dom.actionsTip.d.style("overflow", "hidden");
    this.volcanoDom.actions.style("margin-left", "20px").style("padding", "5px");
    this.addActionButton(
      "Confounding factors",
      [GENE_EXPRESSION, DNA_METHYLATION],
      () => this.interactions.confoundersMenu()
    );
    this.addActionButton(
      "Highlight genes",
      [GENE_EXPRESSION, SINGLECELL_CELLTYPE, DNA_METHYLATION],
      () => this.interactions.launchGeneSetEdit()
    );
    this.addActionButton(
      "Statistics",
      [GENE_EXPRESSION, SINGLECELL_CELLTYPE, DNA_METHYLATION],
      () => {
        this.renderStatsMenu();
      },
      { whenOpen: "Hide statistics" }
    );
    const sigLabel = this.termType == DNA_METHYLATION ? "Number of significant promoters" : "Number of significant genes";
    const numSigGenes = this.viewData.statsData.find((d) => d.label == sigLabel)?.value;
    if (numSigGenes) {
      const sigText = this.termType == DNA_METHYLATION ? `${numSigGenes} DM promoters:` : `${numSigGenes} DE genes:`;
      this.volcanoDom.actions.append("span").text(sigText).style("margin-left", "10px").style("font-weight", "bold");
      const pValueTableButtonText = this.settings.showPValueTable ? "Hide p-value table" : "Show p-value table";
      this.addActionButton(pValueTableButtonText, [GENE_EXPRESSION, SINGLECELL_CELLTYPE, DNA_METHYLATION], async () => {
        const showTable = !this.settings.showPValueTable;
        await this.interactions.app.dispatch({
          type: "plot_edit",
          id: this.interactions.id,
          config: { settings: { volcano: { showPValueTable: showTable } } }
        });
      });
    }
    if (numSigGenes && numSigGenes >= 3) {
      this.addActionButton(
        `Hierarchical clustering of ${numSigGenes > 100 ? "top 100" : numSigGenes} DE genes`,
        [GENE_EXPRESSION],
        async () => {
          await this.interactions.launchDEGClustering();
        }
      );
    }
  }
  /** Use the termTypes arr to render the buttons in a consistent order.
   *
   * Pass `opts.whenOpen` to make the button a toggle: clicking once opens
   * the actionsTip with the callback's content and swaps the button text
   * to `whenOpen` ("Hide statistics", etc.); clicking again hides the tip
   * and restores the original text. The text also restores when the tip
   * closes via Esc or outside-click (Menu.onHide hook), and when another
   * action button hijacks the tip (the loop below resets all toggles
   * before showing the new content). */
  addActionButton(text, termTypes, callback, opts) {
    if (this.viewData.userActions.noShow.has(text)) return;
    if (!termTypes.includes(this.termType)) return;
    const button = this.volcanoDom.actions.append("button").attr("class", "sja_menuoption").style("margin", "3px").style("padding", "3px").text(text).on("click", async () => {
      const whenOpen = opts?.whenOpen;
      if (whenOpen && button.text() === whenOpen) {
        this.dom.actionsTip.hide();
        return;
      }
      this.volcanoDom.actions.selectAll('button[data-volcano-toggle-open="1"]').each(function() {
        const b = select_default(this);
        const closed = b.attr("data-volcano-toggle-closed");
        if (closed) b.text(closed).attr("data-volcano-toggle-open", null);
        this.parent_menu = void 0;
        const eh = this.__volcanoEscHandler;
        if (eh) {
          document.removeEventListener("keydown", eh);
          this.__volcanoEscHandler = void 0;
        }
      });
      this.dom.actionsTip.clear().showunder(button.node());
      if (whenOpen) {
        button.text(whenOpen).attr("data-volcano-toggle-open", "1").attr("data-volcano-toggle-closed", text);
        button.node().parent_menu = this.dom.actionsTip.dnode;
        const escHandler = (e) => {
          if (e.key === "Escape") this.dom.actionsTip.hide();
        };
        document.addEventListener("keydown", escHandler);
        button.node().__volcanoEscHandler = escHandler;
        this.dom.actionsTip.onHide = () => {
          button.text(text).attr("data-volcano-toggle-open", null);
          button.node().parent_menu = void 0;
          document.removeEventListener("keydown", escHandler);
          button.node().__volcanoEscHandler = void 0;
        };
      } else {
        this.dom.actionsTip.onHide = void 0;
      }
      await callback();
    });
  }
  renderPlot(plotDim) {
    this.volcanoDom.svg.attr("width", plotDim.svg.width).attr("height", plotDim.svg.height);
    this.renderTermInfo(plotDim);
    this.volcanoDom.yAxisLabel.attr(
      "transform",
      `translate(${plotDim.yAxisLabel.x}, ${plotDim.yAxisLabel.y}) rotate(-90)`
    );
    this.setSvgSubscriptLabel(this.volcanoDom.yAxisLabel, "-log", "10", `(${this.settings.pValueType} p-value)`);
    this.volcanoDom.xAxisLabel.attr("transform", `translate(${plotDim.xAxisLabel.x}, ${plotDim.xAxisLabel.y})`);
    this.setSvgSubscriptLabel(this.volcanoDom.xAxisLabel, "log", "2", "(fold-change)");
    this.renderScale(plotDim.xScale);
    this.renderScale(plotDim.yScale, true);
    if (this.viewData.volcanoPng) {
      this.volcanoDom.plot.append("image").attr("href", `data:image/png;base64,${this.viewData.volcanoPng}`).attr("x", plotDim.plot.x).attr("y", plotDim.plot.y).attr("width", plotDim.plot.width).attr("height", plotDim.plot.height).attr("preserveAspectRatio", "none");
    }
  }
  renderTermInfo(plotDim) {
    if (this.viewData.termInfo == void 0) return;
    this.volcanoDom.top.attr("transform", `translate(${plotDim.top.x}, ${plotDim.top.y})`);
    const y = this.viewData.termInfo.y;
    const addLabel = (term) => {
      return this.volcanoDom.top.append("text").attr("font-size", "0.9em").attr("transform", `translate(${term.x}, ${y + 10})`).text(term.label);
    };
    const firstTerm = this.viewData.termInfo.first;
    addLabel(firstTerm);
    const secondTerm = this.viewData.termInfo.second;
    const secondLabel = addLabel(secondTerm);
    secondLabel.attr("text-anchor", "end");
  }
  renderScale(scale, isLeft = false) {
    const scaleG = this.volcanoDom[isLeft ? "yAxis" : "xAxis"].append("g").attr("transform", `translate(${scale.x}, ${scale.y})`).call(isLeft ? axisLeft(scale.scale) : axisBottom(scale.scale));
    axisstyle({
      axis: scaleG,
      color: "black",
      showline: true
    });
  }
  renderFoldChangeLine(plotDim) {
    this.volcanoDom.plot.append("line").attr("stroke", "#ccc").attr("shape-rendering", "crispEdges").attr("x1", plotDim.logFoldChangeLine.x).attr("x2", plotDim.logFoldChangeLine.x).attr("y1", plotDim.logFoldChangeLine.y1).attr("y2", plotDim.logFoldChangeLine.y2);
  }
  renderStatsMenu() {
    for (const img of this.viewData.images || []) {
      this.dom.actionsTip.d.append("img").style("display", "inline-block").style("margin-left", "10px").style("margin-top", "-30px").attr("width", 450).attr("height", 450).attr("src", img.src);
    }
    const tableHolder = this.dom.actionsTip.d.append("div").style("display", this.viewData.images.length == 1 ? "inline-block" : "block").style("margin", `${this.viewData.images.length == 1 ? `40px 10px` : `0px 0px`} 0px 5px`).style("vertical-align", "top");
    const table = table2col({ holder: tableHolder });
    for (const d of this.viewData.statsData) {
      const [td1, td2] = table.addRow();
      td1.text(d.label);
      td2.style("text-align", "end").text(d.value);
    }
  }
  renderPValueTable() {
    if (!this.settings.showPValueTable) return;
    const maxTableRows = 5e3;
    const allRows = this.viewData.pValueTableData.rows;
    const rows = allRows.length > maxTableRows ? allRows.slice(0, maxTableRows) : allRows;
    if (allRows.length > maxTableRows) {
      this.volcanoDom.pValueTable.append("div").style("padding", "5px 10px").style("font-size", ".8em").style("color", "#666").text(
        `Showing top ${maxTableRows.toLocaleString()} of ${allRows.length.toLocaleString()} significant results (sorted by fold-change)`
      );
    }
    renderTable({
      columns: this.viewData.pValueTableData.columns,
      rows,
      div: this.volcanoDom.pValueTable,
      showLines: true,
      maxHeight: `${this.viewData.pValueTableData.height}px`,
      resize: true,
      header: { allowSort: true },
      noRadioBtn: true,
      noButtonCallback: (i) => {
        const gene = this.viewData.pValueTableData.rows[i][0].value;
        if (!gene) return;
        this.interactions.highlightDataPoint(gene);
      },
      hoverEffects: (tr, row) => {
        const circles = this.volcanoDom.plot.selectAll("circle").nodes();
        const dataKey = this.termType === DNA_METHYLATION ? "promoter_id" : "gene_name";
        const circle = circles.find((d) => d.__data__[dataKey] == row[0].value);
        if (!circle || circle.__data__.highlighted) return;
        let clone;
        tr.on("mouseover", () => {
          if (circle.__data__.highlighted || clone) return;
          clone = this.volcanoDom.plot.node()?.appendChild(circle.cloneNode(true));
          clone.setAttribute("fill-opacity", 0.9);
        });
        tr.on("mouseleave", () => {
          if (!clone) return;
          clone.remove();
          clone = null;
        });
        this.volcanoDom.pValueTable.on("mouseover", () => {
          selectAll_default(circles).attr("stroke-opacity", 0.075);
        });
        this.volcanoDom.pValueTable.on("mouseleave", () => {
          selectAll_default(circles).attr("stroke-opacity", (d) => d.significant ? 0.35 : 0.2);
        });
      }
    });
  }
  setSvgSubscriptLabel(textElem, prefix, subscript, suffix) {
    textElem.text(null);
    textElem.append("tspan").text(prefix);
    textElem.append("tspan").attr("baseline-shift", "sub").attr("font-size", "0.7em").text(subscript);
    textElem.append("tspan").text(suffix);
  }
  attachInteractions(plotDim) {
    const points = this.viewData.pointData;
    if (!points || points.length === 0) return;
    const dotRadiusPx = this.viewData.plotExtent.dotRadiusPx;
    const hitRadius = dotRadiusPx + 3;
    const highlightRadius = Math.max(0.5, dotRadiusPx - 0.5);
    const highlightColor = this.settings.defaultHighlightColor;
    const hoverLayer = this.volcanoDom.plot.append("g").attr("id", "sjpp-volcano-hover").style("pointer-events", "none");
    const cover = this.volcanoDom.plot.append("rect").attr("id", "sjpp-volcano-cover").attr("x", plotDim.plot.x).attr("y", plotDim.plot.y).attr("width", plotDim.plot.width).attr("height", plotDim.plot.height).attr("fill", "transparent").style("pointer-events", "all").style("cursor", "default");
    const circlePath = (r) => `M${r},0 A${r},${r} 0 1,1 ${-r},0 A${r},${r} 0 1,1 ${r},0 Z`;
    new DataPointInteractions({
      cover,
      hoverLayer,
      hoverTip: this.dom.tip,
      points,
      // Quadtree in cover-local space — d.x/d.y are SVG-absolute, so subtract
      // the plot rect's origin once when building the tree.
      getX: (d) => d.x - plotDim.plot.x,
      getY: (d) => d.y - plotDim.plot.y,
      hitRadius,
      toHoverSpec: (d) => ({
        path: circlePath(highlightRadius),
        // Hover layer lives in the same coord space as the dots (SVG-absolute),
        // so translate by d.x/d.y — NOT the cover-local pair.
        transform: `translate(${d.x},${d.y})`,
        fill: highlightColor,
        fillOpacity: 0.9,
        stroke: "none"
      }),
      maxTooltipRows: this.settings.maxTooltipGenes,
      itemNoun: "gene",
      renderSingleHoverTooltip: (d, container) => {
        const table = table2col({ holder: container.append("table") });
        this.addTooltipRows(d, table);
      },
      buildMultiHitTableData: (dots) => this.buildMultiHitTable(dots),
      getActions: (d) => this.getActionMenuOpts(d),
      renderSingleHitInfo: (d, container) => {
        const tbl = table2col({ holder: container.append("table") });
        this.addTooltipRows(d, tbl);
      },
      getRowKey: (d) => d.gene_name
    }).attach();
  }
  buildMultiHitTable(dots) {
    const isDM = this.termType === DNA_METHYLATION;
    const isDAP = this.termType === PROTEOME_DAP;
    const pValueType = this.settings.pValueType;
    const pLabel = `${pValueType.charAt(0).toUpperCase()}${pValueType.slice(1)} p-value`;
    const pField = `${pValueType}_p_value`;
    const columns = isDM ? [
      { label: "Promoter" },
      { label: "Gene(s)" },
      { label: "log\u2082(FC)", sortable: true },
      { label: pLabel, sortable: true }
    ] : isDAP ? [
      { label: "Identifier" },
      { label: "Gene" },
      { label: "log\u2082(FC)", sortable: true },
      { label: pLabel, sortable: true }
    ] : [{ label: "Gene" }, { label: "log\u2082(FC)", sortable: true }, { label: pLabel, sortable: true }];
    const rows = dots.map((d) => {
      const fc = { value: roundValueAuto(d.fold_change) };
      const pval = { value: roundValueAuto(d[pField]) };
      if (isDM) {
        return [{ value: d.promoter_id || "" }, { value: d.gene_name || "" }, fc, pval];
      }
      if (isDAP) {
        return [{ value: d.gene_name || "" }, { value: d.gene || "" }, fc, pval];
      }
      return [{ value: d.gene_name || "" }, fc, pval];
    });
    return { columns, rows };
  }
  /** Per-data-point action menu items (Violin / DMR / Box-plot). Used by
   * both the single-gene click flow and the multi-gene click-menu rows so
   * the launchers stay in lock-step. */
  getActionMenuOpts(d) {
    const termType = this.termType;
    const interactions = this.interactions;
    const all = [
      {
        label: "Violin plot",
        isVisible: () => termType === DNA_METHYLATION || termType === GENE_EXPRESSION,
        onClick: async () => {
          if (termType === DNA_METHYLATION) interactions.launchDNAMethViolin(d);
          if (termType === GENE_EXPRESSION) interactions.launchViolinGeneExp(d.gene_name);
        }
      },
      {
        label: "DMR analysis",
        isVisible: () => termType === DNA_METHYLATION,
        onClick: async () => {
          const dm = d;
          await interactions.launchDmr({
            chr: dm.chr,
            start: dm.start,
            stop: dm.stop,
            promoterId: dm.promoter_id
          });
        }
      },
      {
        label: "Box plot",
        isVisible: () => termType === GENE_EXPRESSION,
        onClick: async () => {
          interactions.launchBoxPlot(d.gene_name);
        }
      }
    ];
    return all.filter((o) => o.isVisible()).map(({ label, onClick }) => ({ label, onClick }));
  }
  /** Populates a `table2col` instance with the standard volcano hover rows
   * (gene/promoter, fold-change, original + adjusted p-values). */
  addTooltipRows(d, table) {
    if (this.termType === DNA_METHYLATION) {
      if ("promoter_id" in d) addTooltipRow(table, "Promoter", d.promoter_id);
      if (d.gene_name) addTooltipRow(table, "Gene(s)", d.gene_name);
    } else if (this.termType === PROTEOME_DAP) {
      addTooltipRow(table, "Identifier", d.gene_name);
      if ("gene" in d) addTooltipRow(table, "Gene", d.gene);
    } else {
      addTooltipRow(table, "Gene name", d.gene_name);
    }
    addTooltipRow(table, "log<sub>2</sub>(fold-change)", roundValueAuto(d.fold_change));
    addTooltipRow(table, "Original p-value", roundValueAuto(d.original_p_value));
    if (d.adjusted_p_value != void 0) addTooltipRow(table, "Adjusted p-value", roundValueAuto(d.adjusted_p_value));
  }
};
function addTooltipRow(table, text, value) {
  const [td1, td2] = table.addRow();
  td1.html(text);
  td2.text(value);
}
function renderDataPoints(self) {
  self.volcanoDom.plot.selectAll("circle").data(self.viewData.pointData).enter().append("circle").attr("stroke", (d) => rgb(d.color).formatHex()).attr("stroke-opacity", (d) => d.significant ? 0.35 : 0.2).attr("stroke-width", 1).attr("fill", self.settings.defaultHighlightColor).attr("fill-opacity", (d) => d.highlighted ? 0.9 : 0).attr("cx", (d) => d.x).attr("cy", (d) => d.y).attr("r", (d) => d.radius).style("pointer-events", "none");
}

// plots/volcano/VolcanoControlInputs.ts
var VolcanoControlInputs = class {
  constructor(config, termType) {
    this.config = config;
    if (this.config.termType == GENE_EXPRESSION) this.sampleNum = getSampleNum(config);
    this.termType = termType;
    this.inputs = [
      {
        label: "P value significance (-log\u2081\u2080)",
        type: "number",
        chartType: "volcano",
        settingsKey: "pValue",
        title: "The p-value threshold to determine statistical significance",
        min: 0,
        // 5e-324 is the smallest positive number greater than 0 representable
        // in IEEE 64-bit floating point (i.e. javascripts native Number.MIN_VALUE)
        // -Math.log10(5e-324) = 323.3
        max: 323.3,
        step: 1
      },
      {
        label: "P value",
        type: "radio",
        chartType: "volcano",
        settingsKey: "pValueType",
        title: "Toggle between original and adjusted pvalues for volcano plot",
        options: [
          { label: "Adjusted", value: "adjusted" },
          { label: "Original", value: "original" }
        ]
      },
      {
        label: "Fold change (log\u2082)",
        type: "number",
        chartType: "volcano",
        settingsKey: "foldChangeCutoff",
        title: "The fold change threshold to determine biological significance",
        min: -10,
        max: 10
      },
      {
        label: "Max interactive dots",
        type: "number",
        chartType: "volcano",
        settingsKey: "maxInteractiveDots",
        title: "Cap on the number of top-significant points the server returns as interactive overlay circles. The PNG still shows every dot.",
        min: 0,
        max: 2e4,
        step: 100
      },
      //Preferably, keep all the display (e.g. colors, sizes, etc.) controls
      //at the bottom of the list or at least together
      {
        label: "Plot height",
        type: "number",
        chartType: "volcano",
        settingsKey: "height",
        title: "Height of the plot in pixels",
        min: 300,
        max: 1e3
      },
      {
        label: "Plot width",
        type: "number",
        chartType: "volcano",
        settingsKey: "width",
        title: "Width of the plot in pixels",
        min: 300,
        max: 1e3
      },
      {
        label: "Significant value color",
        type: "color",
        chartType: "volcano",
        title: "Default color for significant data points.",
        settingsKey: "defaultSignColor",
        getDisplayStyle: () => {
          if (this.config.termType == SINGLECELL_CELLTYPE) return "none";
          const controlColor = this.config.tw?.term?.values?.[this.config.samplelst.groups[0].name]?.color;
          const caseColor = this.config.tw?.term?.values?.[this.config.samplelst.groups[1].name].color;
          if (controlColor && caseColor) return "none";
          else return "";
        }
      },
      {
        label: "Non-significant value color",
        type: "color",
        chartType: "volcano",
        title: "Default color for non-significant data points.",
        settingsKey: "defaultNonSignColor"
      },
      {
        label: "Highlight color",
        type: "color",
        chartType: "volcano",
        title: "Default color for highlighted data points.",
        settingsKey: "defaultHighlightColor"
      }
    ];
    this.setVolcanoControlInputs();
  }
  /** Add more term type specific controls here. */
  setVolcanoControlInputs() {
    this.addGeneExpControlInputs();
    this.addDNAMethControlInputs();
    this.addSingleCellCTControlInputs();
  }
  addGeneExpControlInputs() {
    if (this.termType !== GENE_EXPRESSION) return;
    const geInputs = [
      {
        label: "Minimum read count",
        type: "number",
        chartType: "volcano",
        settingsKey: "minCount",
        title: "The smallest number of reads required for a gene to be considered in the analysis",
        min: 0,
        max: 1e4
      },
      {
        label: "Minimum total read count",
        type: "number",
        chartType: "volcano",
        settingsKey: "minTotalCount",
        title: "The smallest total number of reads required for a gene to be considered in the analysis",
        min: 0,
        max: 1e4
      },
      {
        label: "CPM cutoff",
        type: "number",
        chartType: "volcano",
        settingsKey: "cpmCutoff",
        title: "The minimum normalized expression threshold to retain only genes with sufficient expression",
        min: 0
      },
      {
        label: "Method",
        type: "radio",
        chartType: "volcano",
        settingsKey: "method",
        title: "Toggle between analysis methods",
        options: this.getMethodOptions()
      }
      // {
      // 	label: 'Rank Genes by',
      // 	type: 'radio',
      // 	chartType: 'volcano',
      // 	settingsKey: 'rankBy',
      // 	title: 'Rank genes by either the absolute value of the fold change or the variance',
      // 	options: [
      // 		{ label: 'abs(Fold Change)', value: 'abs(foldChange)' },
      // 		{ label: 'Variance', value: 'variance' }
      // 	],
      // 	//TODO: will enable this feature when there is backhand support
      // 	getDisplayStyle: () => 'none'
      // }
    ];
    this.inputs.splice(0, 0, ...geInputs);
  }
  addDNAMethControlInputs() {
    if (this.termType !== DNA_METHYLATION) return;
    const dmInputs = [
      {
        label: "Min samples per group",
        type: "number",
        chartType: "volcano",
        settingsKey: "minSamplesPerGroup",
        title: "Minimum non-NA samples required per group for a promoter to be tested",
        min: 1,
        max: 100
      }
    ];
    this.inputs.splice(0, 0, ...dmInputs);
  }
  addSingleCellCTControlInputs() {
    if (this.termType !== SINGLECELL_CELLTYPE) return;
    const scctInputs = [];
    this.inputs.splice(0, 0, ...scctInputs);
  }
  getMethodOptions() {
    if (this.termType !== GENE_EXPRESSION) return;
    const settings = this.config.settings.volcano;
    const features = JSON.parse(sessionStorage.getItem("optionalFeatures"));
    if (features?.runDE_methods?.length) {
      const opts = [];
      for (const m of features.runDE_methods) {
        opts.push({ label: m, value: m.toLowerCase() });
      }
      return opts;
    }
    if (this.sampleNum < settings.sampleNumCutoff) {
      return [
        { label: "edgeR", value: "edgeR" },
        { label: "Wilcoxon", value: "wilcoxon" },
        { label: "Limma", value: "limma" }
      ];
    } else return [{ label: "Wilcoxon", value: "wilcoxon" }];
  }
};

// plots/volcano/Volcano.ts
var Volcano = class _Volcano extends PlotBase {
  static {
    this.type = "volcano";
  }
  constructor(opts, api) {
    super(opts, api);
    if (this.opts.parentId) this.parentId = this.opts.parentId;
    this.type = _Volcano.type;
    this.components = {
      controls: {}
    };
    this.termType = opts.termType;
    const holder = opts.holder.classed("sjpp-volcano-main", true).attr("data-testid", `sjpp-volcano-main-${opts.termType}`);
    const controls = typeof opts.controls == "object" ? opts.controls : holder || holder.append("div");
    const error = opts.holder.append("div").attr("id", "sjpp-volcano-error").attr("data-testid", `sjpp-volcano-error-${opts.termType}`).style("opacity", 0.75);
    this.dom = {
      holder,
      controls,
      error,
      wait: holder.append("div").attr("id", "sjpp-volcano-wait").attr("data-testid", `sjpp-volcano-wait-${opts.termType}`).style("opacity", 0.75).style("padding", "20px").text("Loading..."),
      tip: new Menu({ padding: "" }),
      actionsTip: new Menu({ padding: "" })
    };
  }
  getState(appState) {
    const config = appState.plots.find((p) => p.id === this.id);
    if (!config) {
      throw new Error(
        `No plot with id='${this.id}' found. Did you set this.id before this.api = getComponentApi(this)?`
      );
    }
    const parentConfig = this.parentId && appState.plots.find((p) => p.id === this.parentId);
    const termfilter = getCombinedTermFilter(appState, config.filter || parentConfig?.filter);
    return {
      config: Object.assign({}, config, {
        settings: {
          volcano: config.settings.volcano
        }
      }),
      termfilter
    };
  }
  async setControls() {
    const plotConfig = this.app.getState().plots.find((p) => p.id === this.id);
    const controls = new VolcanoControlInputs(plotConfig, this.termType);
    this.components.controls = await controlsInit({
      app: this.app,
      id: this.id,
      holder: this.dom.controls.style("display", "inline-block"),
      inputs: controls.inputs
    });
    this.components.controls.on("downloadClick.volcano", () => this.interactions.download(this.termType));
    if (plotConfig.chartType == "differentialAnalysis")
      this.components.controls.on(
        "helpClick.differentialAnalysis",
        () => (
          //Opens the page for the differential analysis wiki
          //Can't put in parent as DA does not have a controls component
          window.open("https://github.com/stjude/proteinpaint/wiki/Differential-analysis")
        )
      );
  }
  async init() {
    this.interactions = new VolcanoInteractions(this.app, this.id, this.dom);
    this.model = new VolcanoModel(this.app, this.termType);
    this.view = new VolcanoPlotView(this.dom, this.interactions, this.termType);
    await this.setControls();
  }
  async main() {
    if (!this.interactions) throw new Error("Volcano Interactions not initialized");
    if (!this.model) throw new Error("Volcano Model not initialized");
    if (!this.view) throw new Error("Volcano View not initialized");
    const config = structuredClone(this.state.config);
    if (config.chartType != this.type && config.childType != this.type) return;
    const settings = config.settings.volcano;
    try {
      const showWait = setTimeout(() => {
        this.dom.wait.style("display", "block");
      }, 500);
      const response = await this.model.getData(config, settings);
      this.dom.error.text("");
      if (!response || response.error || !response.data || !response.data.volcanoPng || !response.data.totalRows) {
        const msg = response?.error || "No data returned from server";
        if (response?.code === "CACHE_BUSY") {
          if (window.confirm(msg)) this.main();
        } else sayerror(this.dom.error, msg);
        clearTimeout(showWait);
        this.dom.wait.style("display", "none");
        return;
      }
      const viewModel = new VolcanoViewModel(config, response, settings);
      this.interactions.pValueTableData = viewModel.viewData.pValueTableData;
      this.interactions.data = response.data.dots;
      this.view.render(settings, viewModel.viewData);
      if (!response.data.dots.length) {
        this.dom.error.text("No points passed the significance thresholds").style("color", "#555");
      }
      clearTimeout(showWait);
      this.dom.wait.style("display", "none");
    } catch (e) {
      if (e instanceof Error) console.error(e.message || e);
      else if (e.stack) console.log(e.stack);
      throw e;
    }
  }
};
var volcanoInit = getCompInit(Volcano);
var componentInit = volcanoInit;
async function getPlotConfig(opts, app) {
  if (!opts.termType) throw new Error(".termType is required");
  const config = {
    settings: {
      volcano: getDefaultVolcanoSettings(opts.overrides, opts)
    },
    highlightedData: opts.highlightedData || [],
    termType: opts.termType
  };
  if (opts.termType == GENE_EXPRESSION) {
    if (opts.confounderTws) {
      try {
        for (const tw of opts.confounderTws) {
          await fillTermWrapper(tw, app.vocabApi);
        }
      } catch (e) {
        console.error(e.message || e);
        throw new Error(`Volcano getPlotConfig() failed to fill confounder term wrappers: ${e}`);
      }
    }
    Object.assign(config, {
      confounderTws: opts.confounderTws || [],
      samplelst: opts.samplelst
    });
  }
  if (opts.termType == SINGLECELL_CELLTYPE) {
    Object.assign(config, {
      //TODO: Fix this logic
      sample: opts.experimentID || opts.sample || opts.samples?.[0]?.experiments[0]?.experimentID,
      termId: app.vocabApi.termdbConfig.queries.singleCell.DEgenes.termId,
      //TODO: 'Cluster' is a fallback for development
      //Should require opts.categoryName in the future
      categoryName: opts.categoryName || "Cluster"
    });
  }
  validateVolcanoSettings(config, opts);
  return copyMerge(config, opts);
}
export {
  componentInit,
  getPlotConfig,
  volcanoInit
};
//# sourceMappingURL=Volcano-YH4RJTT5.js.map
