import {
  PlotBase,
  digestMessage,
  getGEunit,
  make_radios,
  newSandboxDiv,
  renderTable
} from "./chunk-SA7APTJR.js";
import "./chunk-HJ6L54YS.js";
import "./chunk-LSEFWW72.js";
import {
  importPlot
} from "./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 {
  dofetch3
} from "./chunk-7EMWHCVW.js";
import "./chunk-7IYJZZQI.js";
import {
  copyMerge,
  getCompInit
} from "./chunk-M3J4MINX.js";
import "./chunk-PF4DSFDR.js";
import "./chunk-QH6W3NVF.js";
import {
  SINGLECELL_CELLTYPE,
  SINGLECELL_GENE_EXPRESSION
} from "./chunk-NOEAT6CX.js";
import {
  TermTypeGroups
} from "./chunk-4UWS5Y3N.js";
import "./chunk-DD4R5P6W.js";
import "./chunk-JNITUVXP.js";
import "./chunk-KSGA62R2.js";
import "./chunk-LOZEKOES.js";
import "./chunk-TOU7EVFQ.js";
import "./chunk-OAWQ6LOO.js";
import "./chunk-TLT4YIG3.js";
import "./chunk-KYBIQBXE.js";
import "./chunk-I6Y4O3RR.js";
import "./chunk-OMR2DT66.js";
import "./chunk-DQC5FFGV.js";
import "./chunk-HFNDKYVF.js";

// plots/sc/model/SCModel.ts
var SCModel = class {
  constructor(app, id) {
    this.app = app;
    this.id = id;
    this.state = this.app.getState();
  }
  /********** Single Cell SAMPLES for rendering the table ********
   * The table data does not update. Should only need to run once. */
  async getSampleData() {
    const body = this.getSampleRequestOpts();
    return await dofetch3("termdb/singlecellSamples", { body });
  }
  //May involve more complicated logic later
  getSampleRequestOpts() {
    return {
      genome: this.state.vocab.genome,
      dslabel: this.state.vocab.dslabel,
      filter0: this.state.termfilter.filter0 || null
    };
  }
  //Fetches optional name for ds defined columns
  async getColumnLabels(dsScSamples) {
    if (!dsScSamples || !dsScSamples.sampleColumns) return;
    const colsCopy = structuredClone(dsScSamples.sampleColumns);
    for (const col of colsCopy) {
      let label = col.termid;
      try {
        label = (await this.app.vocabApi.getterm(col.termid)).name;
      } catch (e) {
        if (e.message) {
        }
      }
      col.label = label;
    }
    return colsCopy;
  }
  /********** Single Cell DATA for rendering plots ********
   * This is for the plot buttons. Returns an array plots with found files or
   * available data. */
  async getData() {
    const body = this.getDataRequestOpts();
    if (!body) return;
    return await dofetch3("termdb/singlecellData", { body });
  }
  /** May provide active plots to the request and return plot data when
   * checkPlotAvailability is false. When checkPlotAvailability is true,
   * only returns which plots are available but not the actual data. */
  getDataRequestOpts(_plots = [], checkPlotAvailability = true) {
    const state = this.app.getState();
    const singleCellTermdbConfig = state.termdbConfig?.queries?.singleCell;
    if (!singleCellTermdbConfig?.data) throw new Error("No singleCell.data defined in termdbConfig.queries");
    const config = state.plots.find((p) => p.id === this.id);
    if (!config.settings.sc.item) return;
    const plots = _plots?.length ? _plots : singleCellTermdbConfig.data.plots.map((p) => p.name);
    return {
      genome: this.state.vocab.genome,
      dslabel: this.state.vocab.dslabel,
      // if true, only return available plot names, but not actual plot data
      checkPlotAvailability,
      plots,
      sample: {
        eID: config.settings.sc.item.eID,
        sID: config.settings.sc.item.sID
      }
    };
  }
  /** Essentially for the GDC. Maybe applied to other ds in the future. */
  async getCategories(_plots) {
    const body = this.getDataRequestOpts(_plots, false);
    if (!body) return;
    let res;
    try {
      res = await dofetch3("termdb/singlecellData", { body });
    } catch (e) {
      if (e instanceof Error) console.error(`${e.message || e}`);
    }
    return this.formatCategories(res);
  }
  formatCategories(res) {
    const plot = structuredClone(res.plots[0]);
    plot.cells = [...plot.noExpCells, ...plot.expCells];
    const clusters = new Set(plot.cells.map((c) => c.category));
    const sortedClusters = Array.from(clusters).sort((a, b) => {
      const num1 = parseInt(a.split(" ")[1]);
      const num2 = parseInt(b.split(" ")[1]);
      return num1 - num2;
    });
    return sortedClusters;
  }
};

// plots/sc/viewModel/SCViewModel.ts
var SCViewModel = class {
  constructor(app, config, _items, sampleColumns) {
    this.app = app;
    this.state = this.app.getState();
    const items = _items.sort((a, b) => b.isMetaResult === a.isMetaResult ? 0 : b.isMetaResult ? 1 : -1);
    const [rows, columns, sampleColIdx] = this.getTabelData(config, items, sampleColumns);
    const selectedRows = [];
    const sID = config.settings.sc.item?.sID;
    const i = sID ? items.findIndex((item) => item.sample === sID || item.experiments?.some((e) => e.sampleName === sID)) : -1;
    if (i != -1) selectedRows.push(i);
    this.tableData = {
      rows,
      columns,
      selectedRows,
      sampleColIdx
    };
  }
  getTabelData(plotConfig, items, sampleColumns) {
    const rows = [];
    const hasExperiments = items.some((i) => i.experiments);
    let sampleColIdx = -1;
    const columns = [{ label: plotConfig.settings.sc.columns.sample, sortable: true }];
    if (hasExperiments) {
      columns.push({ label: "Sample", sortable: true });
      sampleColIdx = 1;
    } else sampleColIdx = 0;
    columns.push({ label: "Shown plots" });
    for (const col of sampleColumns || []) {
      columns.push({
        label: col.label,
        width: "14vw",
        sortable: true
      });
    }
    if (hasExperiments) columns.push({ label: "Experiment", sortable: true });
    for (const item of items) {
      if (hasExperiments)
        for (const exp of item.experiments) {
          const row = [{ value: item.sample, __experimentID: exp.experimentID }];
          row.push({ value: exp.sampleName });
          row.push({ value: "" });
          for (const col of sampleColumns || []) {
            row.push({ value: item[col.termid] });
          }
          if (this.state.vocab.dslabel == "GDC")
            row.push({ value: exp.experimentID, url: `https://portal.gdc.cancer.gov/files/${exp.experimentID}` });
          else row.push({ value: exp.experimentID });
          rows.push(row);
        }
      else {
        const row = item.isMetaResult ? [{ html: item.sample.replace(/_/g, " "), value: item.sample }] : [{ value: item.sample }];
        row.push({ value: "" });
        for (const col of sampleColumns || []) {
          const value = item[col.termid];
          if (value == null && item.isMetaResult) row.push({ value: "All" });
          else row.push({ value: item[col.termid] });
        }
        rows.push(row);
      }
    }
    return [rows, columns, sampleColIdx];
  }
};

// plots/sc/interactions/SCInteractions.ts
var SCInteractions = class {
  constructor(sc) {
    this.app = sc.app;
    this.dom = sc.dom;
    this.id = sc.id;
    this.model = sc.model;
  }
  /** Used in the gene search menu shown on click from a plot btn
   * Add the plot to the state.plots array with .parentId. Adding
   * .parentId prevents the plot from launching in a new sandbox.
   * Pass the .parentId to both the plotConfig and the action.
   * this.getState() in SC.ts will find all the subplots with the parentId==this.id
   * SC.main() initializes the subplots as components in chartsDiv
   */
  async createSubplot(config) {
    const c = Object.assign({}, config, { parentId: this.id });
    await this.app.dispatch({
      type: "plot_create",
      parentId: this.id,
      config: c
    });
  }
  /** Updates the item in the plot settings */
  async updateItem(item) {
    await this.app.dispatch({
      type: "plot_edit",
      id: this.id,
      config: { settings: { sc: { item } } }
    });
  }
  async getDropDownOptions(plot) {
    return this.model.getCategories(plot);
  }
  toggleLoading(on) {
    if (on) {
      this.dom.loading.selectAll("*").remove();
      this.dom.loading.style("display", "block").append("div").style("position", "relative").style("top", "50%").append("span").attr("class", "sjpp-spinner");
      this.dom.loading.style("display", "");
    } else {
      this.dom.loading.selectAll(".sjpp-spinner").remove();
      this.dom.loading.style("display", "none");
    }
  }
};

// plots/sc/view/SampleTableRenderer.ts
var SampleTableRenderer = class {
  constructor(dom, interactions, tableData) {
    this.sandboxes = /* @__PURE__ */ new Map();
    this.dom = dom;
    this.interactions = interactions;
    this.tableData = tableData;
    this.renderSamplesTable(tableData);
  }
  /** Users select one item at a time to render the plot buttons
   * to init() plots in the dashboard.*/
  renderSamplesTable(tableData) {
    renderTable({
      rows: tableData.rows,
      columns: tableData.columns,
      div: this.dom.tableDiv,
      singleMode: true,
      maxWidth: tableData.columns.length > 3 ? "95vw" : "auto",
      maxHeight: "30vh",
      header: {
        allowSort: true,
        style: { "text-transform": "capitalize" }
      },
      striped: true,
      selectedRows: tableData.selectedRows,
      afterRender: () => {
        this.reapplyAllPlotButtons();
      },
      noButtonCallback: (index) => {
        const item = this.buildItemFromRow(tableData, index);
        this.interactions.updateItem(item);
        this.dom.plotsBtnsDiv.style("display", "block");
      }
    });
  }
  /** Builds an item object from a table row, mapping column labels to keys.
   * Converts 'sample' -> 'sID' and 'experiment' -> 'eID'.
   * Extracted out from noButtonCallback for testing.  */
  buildItemFromRow(tableData, index) {
    const item = {};
    tableData.rows[index].forEach((r, idx) => {
      if (!r.value) return;
      let key = tableData.columns[idx].label.toLowerCase();
      key = key === "sample" ? "sID" : key === "experiment" ? "eID" : key;
      item[key] = r.value;
    });
    if (!item.sID) throw new Error("Selected item must have sID property");
    return item;
  }
  updateTable(sampleId, sandboxes) {
    this.sandboxes = sandboxes;
    this.applyButtonsForSample(sampleId);
  }
  /** Called by afterRender to re-apply buttons for all samples with subplots. */
  reapplyAllPlotButtons() {
    for (const sampleId of this.sandboxes.keys()) {
      this.applyButtonsForSample(sampleId);
    }
  }
  /** Applies buttons for a single sample after plot button click */
  applyButtonsForSample(sampleId) {
    const sampleIdx = this.tableData.sampleColIdx;
    if (!this.sandboxes) return;
    const row = this.tableData.rows.find((r) => r[sampleIdx].value === sampleId);
    if (!row) return;
    const cell = row[sampleIdx + 1].__td;
    cell.selectAll(".sjpp-sc-table-plot-btn").remove();
    const sampleSandboxes = this.sandboxes.get(sampleId);
    if (!sampleSandboxes || sampleSandboxes.length === 0) return;
    for (const { div, plotName } of sampleSandboxes) {
      this.appendPlotBtn(cell, div, plotName, sampleId);
    }
  }
  appendPlotBtn(cell, sandboxDiv, plotName, sampleId) {
    const text = plotName.length > 25 ? plotName.slice(0, 12) + "..." : plotName;
    const label = `Scroll to ${plotName}`;
    cell.append("button").attr("class", "sjpp-sc-table-plot-btn").attr("data-testid", `sjpp-sc-table-${sampleId}-${plotName}-btn`).style("padding", "2px 5px").style("margin-left", "4px").style("font-size", "0.8em").style("border-radius", "20px").style("border", "0.5px solid black").style("cursor", "pointer").text(text).attr("aria-label", label).attr("title", label).attr("tabindex", 0).on("click", () => {
      sandboxDiv.node().scrollIntoView({ behavior: "smooth", block: "start" });
    });
  }
};

// plots/sc/view/PlotButtons.ts
var PlotButtons = class {
  /** This is the initial state. scctTerms and the termdbConfig are created on
   * server init and will not change. */
  constructor(interactions, holder, state) {
    holder.style("padding", "10px");
    const promptDiv = holder.append("div").style("padding", "10px 0").text("Select data from");
    this.plotBtnsDom = {
      promptDiv,
      selectPrompt: promptDiv.append("span"),
      btnsDiv: holder.append("div"),
      tip: new Menu({ padding: "" })
    };
    this.interactions = interactions;
    this.scctTerms = state.termdbConfig?.termType2terms?.[TermTypeGroups.SINGLECELL_CELLTYPE];
    this.scTermdbConfig = state.termdbConfig.queries.singleCell;
  }
  update(settings, data) {
    const item = settings.sc.item;
    this.plotBtnsDom.promptDiv.style("display", !item ? "none" : "block");
    if (!item) return;
    if (data != null && data.plots) this.data = data;
    this.availablePlots = new Set(this.data?.plots?.map((p) => p.name));
    this.settings = settings;
    this.item = item;
    const name = item.sID;
    this.plotBtnsDom.selectPrompt.text(` ${name}:`);
    this.renderChartBtns();
  }
  renderChartBtns() {
    this.plotBtnsDom.btnsDiv.selectAll("*").remove();
    const btns = this.getChartBtnOpts();
    this.plotBtnsDom.btnsDiv.selectAll("button").data(btns.filter((b) => b.isVisible())).enter().append("button").attr("type", "button").attr("data-testid", (b) => `sjpp-sc-plot-btn-${b.label.toLowerCase().replace(/\s/g, "-")}`).style("padding", "10px 15px").style("border-radius", "20px").style("border-color", "transparent").style("background-color", "#CFE2F3").style("margin", "0 10px").style("cursor", "pointer").text((b) => b.label).on("click", async (e, plot) => {
      if (plot.open) {
        this.plotBtnsDom.tip.clear().showunder(e.target);
        plot.open(plot, this);
      } else {
        if (!plot.getPlotConfig)
          throw new Error(`No getPlotConfig function defined for this plot button = ${plot.label}`);
        const config = await plot.getPlotConfig();
        await this.interactions.createSubplot(config);
      }
    });
  }
  getChartBtnOpts() {
    const btns = [];
    for (const plot of this.scTermdbConfig?.data?.plots || []) {
      btns.push({
        label: plot.name,
        isVisible: () => this.availablePlots.has(plot.name),
        getPlotConfig: async () => {
          return await this.getSingleCellConfig(plot.name);
        }
      });
    }
    btns.push(
      {
        label: "Summary",
        isVisible: () => true,
        getPlotConfig: () => {
          const sample = { ...this.item, plots: Array.from(this.availablePlots) };
          return {
            chartType: "dictionary",
            sample,
            spawnConfig: {
              parentId: this.interactions.id,
              hidePlotFilter: true,
              headerText: `Sample: ${this.item.sID}`,
              sample
            },
            tree: {
              usecase: {
                target: "dictionary",
                specialCase: {
                  type: "singleCell",
                  config: { sample }
                }
              }
            }
          };
        }
      },
      {
        label: "Gene expression",
        isVisible: () => this.scTermdbConfig?.geneExpression,
        getPlotConfig: () => {
          const sample = this.item;
          const headerText = `Sample: ${this.item.sID}`;
          return {
            chartType: "GeneExpInput",
            termType: SINGLECELL_GENE_EXPRESSION,
            headerText,
            termProperties: { sample },
            sample,
            spawnConfig: {
              parentId: this.interactions.id,
              hidePlotFilter: true,
              headerText
            }
          };
        }
      },
      {
        label: "Differential expression",
        isVisible: () => this.scTermdbConfig?.DEgenes,
        open: this.termDropdownMenu,
        getPlotConfig: (value) => {
          return {
            chartType: "differentialAnalysis",
            termType: SINGLECELL_CELLTYPE,
            categoryName: `${value}`,
            headerText: `Sample: ${this.item.sID} ${this.scTermdbConfig.DEgenes.termId} ${value}`,
            termId: this.scTermdbConfig.DEgenes.termId,
            sample: this.item
          };
        }
      },
      {
        label: this.scTermdbConfig?.images?.label || "Image",
        isVisible: () => this.scTermdbConfig?.images && this.availablePlots.has(this.scTermdbConfig.images.label || "Image"),
        getPlotConfig: () => {
          return {
            chartType: "imagePlot",
            sample: this.item,
            imgDir: this.scTermdbConfig?.images,
            headerText: `Sample: ${this.item.sID}`,
            settings: { imagePlot: { width: "", height: 400 } }
          };
        }
      }
    );
    return btns;
  }
  //********** Btn Menus **********/
  async termDropdownMenu(plot, self) {
    const _plot = Array.from(self.availablePlots)[0];
    const options = await self.interactions.getDropDownOptions([_plot]);
    if (!options?.length) throw new Error("No options found for this plot. Cannot open dropdown menu.");
    self.plotBtnsDom.tip.clear();
    const wrapper = self.plotBtnsDom.tip.d.append("div").style("padding", "10px");
    wrapper.append("div").style("display", "block").style("width", "300px").text(`View differentially expressed genes of a ${self.scTermdbConfig.DEgenes.termId} versus rest of the cells:`);
    const select = wrapper.append("select").style("margin", "10px 0").style("width", "auto").style("padding", "5px").on("change", async function() {
      self.plotBtnsDom.tip.hide();
      const value = select.node().value;
      if (value.indexOf("Select") == 0) return;
      const config = plot.getPlotConfig(value);
      await self.interactions.createSubplot(config);
    });
    const regex = new RegExp(self.scTermdbConfig.DEgenes.termId, "gi");
    options.unshift(`Select a ${self.scTermdbConfig.DEgenes.termId}...`);
    for (const cluster of options) {
      select.append("option").attr("value", cluster.replace(regex, "").trim()).text(cluster);
    }
  }
  //********** Plot Config Helpers **********/
  async getSingleCellConfig(plotName) {
    if (!this.item) throw new Error("No item selected");
    const plot = this.scTermdbConfig.data.plots.find((p) => p.name == plotName);
    if (!plot) throw new Error(`No plot by name ${plotName} in data.plots.`);
    const sample = this.item;
    const config = {
      chartType: "sampleScatter",
      name: `Sample: ${this.item.sID}`,
      sample,
      singleCellPlot: {
        name: plotName,
        sample
      }
    };
    if (plot.colorColumns?.[0]) {
      config.colorTW = await this.makeScctTW(sample, plot);
    }
    return config;
  }
  // Quick fix. Eventually use the handler to get the proper term from the termdbConfig
  async makeScctTW(item, plot) {
    const colorColName = plot.colorColumns[0].name;
    const savedTerm = this.scctTerms?.find((t) => t.name == colorColName && t.plot == plot.name);
    if (!savedTerm)
      throw new Error(
        `No term found for colorColumn=${colorColName} in .termType2terms.[TermTypeGroups.SINGLECELL_CELLTYPE] for plot ${plot.name}`
      );
    const term = Object.assign(structuredClone(savedTerm), {
      sample: item
    });
    const id = await digestMessage(`${plot.name}-${item.sID}-${item.eID}`);
    return Object.assign({ $id: id }, { term });
  }
};

// plots/sc/view/SectionRenderer.ts
var SectionRenderer = class {
  constructor(sectionsDiv, groupBy) {
    this.sections = {};
    this.holder = sectionsDiv;
    this.plotId2Key = /* @__PURE__ */ new Map();
    this.groupBy = groupBy;
  }
  /** Send the sc with the updated state. May not be necessary long term. If not,
   * remove and put in the constructor. */
  async update(sc, subplots, groupBy) {
    if (groupBy !== this.groupBy) {
      this.groupBy = groupBy;
      this.regroupSections(sc, subplots);
      return;
    }
    const activeSubplots = new Set(subplots.map((s) => s.id));
    for (const plotId of Object.keys(sc.components.plots)) {
      if (!activeSubplots.has(plotId)) {
        this.removeSandbox(plotId, sc);
      }
    }
    for (const subplot of subplots) {
      const key = this.getKey(subplot);
      if (!key) continue;
      if (!this.sections[key]) this.initSection(key, sc);
      if (!this.sections[key].sandboxes[subplot.id]) {
        this.plotId2Key.set(subplot.id, key);
        await this.initSandbox(sc, subplot, key);
      }
    }
    for (const key of Object.keys(this.sections)) {
      if (Object.keys(this.sections[key].sandboxes).length === 0) {
        this.removeSection(key, sc);
      }
    }
  }
  /** Reparent existing sandboxes into new section containers
   * without destroying/recreating plot components. */
  regroupSections(sc, subplots) {
    const detached = /* @__PURE__ */ new Map();
    for (const [plotId, key] of this.plotId2Key) {
      const sandboxNode = this.sections[key]?.sandboxes[plotId];
      if (sandboxNode) {
        sandboxNode.remove();
        detached.set(plotId, sandboxNode);
      }
    }
    this.holder.selectAll("*").remove();
    this.sections = {};
    this.plotId2Key = /* @__PURE__ */ new Map();
    const activeSubplots = new Set(subplots.map((s) => s.id));
    for (const plotId of Object.keys(sc.components.plots)) {
      if (!activeSubplots.has(plotId)) {
        sc.removeComponent(plotId);
        detached.delete(plotId);
      }
    }
    for (const subplot of subplots) {
      const key = this.getKey(subplot);
      if (!key) continue;
      if (!this.sections[key]) this.initSection(key, sc);
      this.plotId2Key.set(subplot.id, key);
      const existing = detached.get(subplot.id);
      if (existing) {
        this.sections[key].subplots.node().prepend(existing.node());
        this.sections[key].sandboxes[subplot.id] = existing;
      }
    }
  }
  getKey(subplot) {
    if (this.groupBy === "none") return "none";
    if (this.groupBy === "sample") return this.getSampleId(subplot);
    return this.getPlotName(subplot);
  }
  /** Extract sID from a subplot's config.
   * Actual subplots store sample as {sID, eID} at top level or on term.term.sample. */
  getSampleId(subplot) {
    return subplot.sample?.sID || subplot.singleCellPlot?.sample?.sID || subplot.term?.term?.sample?.sID;
  }
  getPlotName(subplot) {
    let plotName = subplot?.plotName || subplot?.singleCellPlot?.name;
    if (!plotName) {
      if (subplot.chartType === "dictionary") plotName = "Summary";
      else if (subplot.chartType === "summary") plotName = "Summary";
      else if (subplot.chartType === "GeneExpInput") plotName = "Gene expression";
      else if (subplot?.term?.term?.plot) plotName = subplot.term.term.plot;
    }
    return plotName;
  }
  initSection(key, sc) {
    const item = this.findSampleMetadata(key, sc);
    const titleAttrText = this.groupBy == "sample" ? "this sample section" : this.groupBy == "plot" ? "this plot section" : "all plots";
    const sectionWrapper = this.holder.insert("div", ":first-child").style("padding", "10px").attr("data-testid", `sjpp-sc-section-wrapper-${key}`);
    sectionWrapper.append("span").attr("data-testid", `sjpp-sc-section-remove-btn-${key}`).style("margin", "0px 5px").style("cursor", "pointer").attr("title", `Remove ${titleAttrText}`).html(
      `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="#000" class="bi bi-x-lg" viewBox="0 0 12 12">
                <path
                    stroke="#000"
                    transform="scale(0.75)"
                    d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/>
                </svg>`
    ).on("click", () => {
      this.removeSection(key, sc);
    });
    const titleText = this.makeSectionTitleText(key, item);
    const titleWrapper = sectionWrapper.append("span").style("font-weight", 600).style("opacity", 0.7).text(titleText);
    if (titleText.length) {
      const arrow = titleWrapper.append("span").style("font-size", "0.8em").style("padding-left", "3px").attr("title", `Show/hide plots in ${titleAttrText}`).text("\u25BC");
      titleWrapper.on("click", () => {
        const isHidden = this.sections[key].subplots.style("display") === "none";
        this.sections[key].subplots.style("display", isHidden ? "block" : "none");
        arrow.text(isHidden ? "\u25BC" : "\u25B2");
      });
    }
    this.sections[key] = {
      sectionWrapper,
      title: titleWrapper,
      subplots: sectionWrapper.append("div").attr("data-testid", `sjpp-sc-subplots-${key}`),
      sandboxes: {}
    };
  }
  /** Look up sample metadata from the fetched items list.
   * For experiment datasets, matches sID against experiments[].sampleName.
   * For non-experiment datasets, matches sID against item.sample. */
  findSampleMetadata(sampleId, sc) {
    if (!sc.items) return void 0;
    return sc.items.find((item) => item.sample === sampleId || item.experiments?.some((e) => e.sampleName === sampleId));
  }
  makeSectionTitleText(key, item) {
    if (this.groupBy === "none") return "All plots";
    if (this.groupBy === "plot") return key;
    const caseText = item?.sample && item.sample !== key ? `Case: ${item.sample}` : "";
    const itemText = `Sample: ${key}`;
    const projectText = item?.["project id"] ? `Project: ${item["project id"]}` : "";
    return [itemText, caseText, projectText].filter(Boolean).join(" ");
  }
  async initSandbox(sc, subplot, key) {
    const sandboxHolder = this.sections[key].subplots.insert("div", ":first-child").attr("data-testid", `sjpp-sc-sandbox-${subplot.id}`);
    const sandbox = newSandboxDiv(sandboxHolder, {
      close: () => {
        this.removeSandbox(subplot.id, sc);
        sc.app.dispatch({
          type: "plot_delete",
          id: subplot.id,
          parentId: sc.id
        });
      },
      plotId: subplot.id
    });
    const opts = Object.assign({}, subplot, {
      app: sc.app,
      parentId: sc.id,
      id: subplot.id
    });
    if (subplot.chartType == "summary") {
      opts.holder = sandbox;
    } else {
      opts.holder = sandbox.body;
      opts.header = sandbox.header;
    }
    await sc.initPlotComponent(subplot.id, opts);
    this.sections[key].sandboxes[subplot.id] = sandbox.app_div;
  }
  removeSection(key, sc) {
    const subactions = [];
    for (const plotId of Object.keys(this.sections[key].sandboxes || {})) {
      this.removeSandbox(plotId, sc, key);
      subactions.push({
        type: "plot_delete",
        id: plotId,
        parentId: sc.id
      });
    }
    if (subactions.length > 0) {
      sc.app.dispatch({
        type: "app_refresh",
        subactions
      });
    }
    this.sections[key].sectionWrapper.remove();
    delete this.sections[key];
  }
  removeSandbox(plotId, sc, _key) {
    sc.removeComponent(plotId);
    const key = _key || this.plotId2Key.get(plotId);
    if (!key) return;
    this.sections[key].sandboxes[plotId].remove();
    delete this.sections[key].sandboxes[plotId];
    this.plotId2Key.delete(plotId);
  }
  /** Derives a map of sampleId → sandbox info for all active subplots.
   * Used by SampleTableRenderer to render scroll-to buttons. */
  getSampleSandboxes(subplots) {
    const sandboxes = /* @__PURE__ */ new Map();
    for (const subplot of subplots) {
      const sampleId = this.getSampleId(subplot);
      const key = this.plotId2Key.get(subplot.id);
      if (!sampleId || !key) continue;
      const div = this.sections[key]?.sandboxes[subplot.id];
      if (!div) continue;
      const plotName = this.getPlotName(subplot) || subplot.id;
      if (!sandboxes.has(sampleId)) sandboxes.set(sampleId, []);
      sandboxes.get(sampleId).push({ plotId: subplot.id, div, plotName });
    }
    return sandboxes;
  }
};

// plots/sc/settings/Settings.ts
var GroupByOptions = ["none", "sample", "plot"];

// plots/sc/view/SCViewRenderer.ts
var SCViewRenderer = class _SCViewRenderer {
  static {
    //On load, show table
    //Eventually maybe an app dispatch and not a flag
    this.inUse = true;
  }
  constructor(sc, state) {
    this.sc = sc;
    this.dom = sc.dom;
    this.interactions = sc.interactions;
    this.plotBtns = new PlotButtons(this.interactions, this.dom.plotsBtnsDiv, state);
    this.sectionRenderer = new SectionRenderer(this.dom.sectionsDiv, state.config.settings.sc.groupBy);
  }
  render(tableData, settings) {
    this.renderSelectBtn();
    this.renderGroupByOptions(settings);
    this.sampleTableRenderer = new SampleTableRenderer(this.dom, this.interactions, tableData);
    this.dom.plotsBtnsDiv.style("display", "none");
  }
  /** Renders the select btn at the top of the page that
   * show/hides the item table and plot buttons */
  renderSelectBtn() {
    this.dom.controlsDiv.style("padding", "10px");
    const btn = this.dom.controlsDiv.append("button").attr("data-testid", "sjpp-sc-item-table-select-btn").attr("title", "Show/hide sample table and plot buttons").style("border-radius", "20px").style("padding", "5px 10px").style("background-color", "transparent").text("Select sample and plots");
    const arrowSpan = btn.append("span").style("font-size", "0.8em").style("padding-left", "3px").text("\u25BC");
    btn.on("click", () => {
      _SCViewRenderer.inUse = !_SCViewRenderer.inUse;
      arrowSpan.text(_SCViewRenderer.inUse ? "\u25BC" : "\u25B2");
      this.dom.tableDiv.style("display", _SCViewRenderer.inUse ? "block" : "none");
      this.dom.plotsBtnsDiv.style("display", _SCViewRenderer.inUse ? "block" : "none");
    });
  }
  renderGroupByOptions(settings) {
    this.dom.controlsDiv.append("span").style("padding", "3px 0px 3px 20px").style("opacity", 0.7).text("Group plots by:");
    const optionsDiv = this.dom.controlsDiv.append("span").style("display", "inline-block");
    const options = GroupByOptions.map((option) => {
      return {
        label: `${option.charAt(0).toUpperCase() + option.slice(1)}`,
        value: option,
        checked: settings.groupBy === option
      };
    });
    make_radios({
      holder: optionsDiv,
      styles: { display: "inline-block" },
      options,
      callback: async (value) => {
        await this.sc.app.dispatch({
          type: "plot_edit",
          id: this.sc.id,
          config: { settings: { sc: { ...settings, groupBy: value } } }
        });
      }
    });
  }
  async update(settings, data, subplots) {
    this.plotBtns.update(settings, data);
    await this.sectionRenderer.update(this.sc, subplots, settings.sc.groupBy);
    if (!settings.sc.item?.sID) return;
    const sandboxes = this.sectionRenderer.getSampleSandboxes(subplots);
    this.sampleTableRenderer.updateTable(settings.sc.item.sID, sandboxes);
  }
};

// plots/sc/settings/defaults.ts
function getDefaultSCAppSettings(overrides = {}, app) {
  const defaults = {
    sc: {
      columns: {
        // TODO: Implement ds specific column name
        sample: "Sample"
      },
      item: void 0,
      groupBy: "sample"
    },
    hierCluster: {
      unit: getGEunit(app.vocabApi),
      yDendrogramHeight: 0,
      clusterSamples: false
    }
  };
  return Object.assign(defaults, overrides);
}

// plots/sc/SC.ts
var SCViewer = class _SCViewer extends PlotBase {
  static {
    this.type = "sc";
  }
  constructor(opts, api) {
    super(opts, api);
    this.type = _SCViewer.type;
    this.components = {
      plots: {}
    };
    const div = opts.holder.classed("sjpp-sc-main", true).append("div").style("padding", "5px").style("display", "inline-block").style("vertical-align", "top");
    this.dom = {
      div,
      loading: opts.holder.append("div").attr("class", "sjpp-sc-main-loading").attr("data-testid", "sjpp-sc-main-loading").style("position", "absolute").style("top", "0").style("left", "0").style("width", "100%").style("height", "100%").style("background-color", "rgba(255, 255, 255, 0.95)").style("text-align", "center"),
      controlsDiv: div.append("div").attr("id", "sjpp-sc-controls-btn"),
      tableDiv: div.append("div").attr("id", "sjpp-sc-item-table"),
      plotsBtnsDiv: div.append("div").attr("id", "sjpp-sc-plot-buttons").style("display", "none"),
      sectionsDiv: div.append("div").attr("id", "sjpp-sc-sections")
    };
    if (opts.header) opts.header.html(`SINGLE CELL`).style("font-size", "0.9em");
  }
  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)?`
      );
    }
    return {
      config,
      subplots: appState.plots.filter((p) => p.parentId === this.id),
      termfilter: appState.termfilter,
      termdbConfig: appState.termdbConfig,
      vocab: appState.vocab
    };
  }
  async init(appState) {
    const state = this.getState(appState);
    const dsScSamples = state.termdbConfig.queries?.singleCell?.samples;
    this.model = new SCModel(this.app, this.id);
    try {
      const response = await this.model.getSampleData();
      if (response.error || !response.samples || !response.samples.length) {
        this.app.printError("No samples found for this dataset");
        return;
      }
      this.items = response.samples;
      this.itemColumns = await this.model.getColumnLabels(dsScSamples);
    } catch (e) {
      if (e instanceof Error) console.error(`${e.message || e} [SC init()]`);
      else if (e.stack) console.log(e.stack);
      throw new Error(e.message || e);
    }
    this.interactions = new SCInteractions(this);
    this.viewModel = new SCViewModel(this.app, state.config, this.items, this.itemColumns);
    this.view = new SCViewRenderer(this, state);
    this.view.render(this.viewModel.tableData, state.config.settings.sc);
  }
  async main() {
    const state = structuredClone(this.state);
    const config = state.config;
    if (!this.model) throw new Error(`Model not initialized`);
    if (!this.viewModel) throw new Error(`ViewModel not initialized`);
    if (!this.view) throw new Error(`View not initialized`);
    if (!this.interactions) throw new Error(`Interactions not initialized`);
    this.interactions.toggleLoading(true);
    let data = null;
    if (config.settings.sc.item) {
      try {
        data = await this.model.getData();
        if (data.error || !data.plots || !data.plots.length) {
          this.interactions.toggleLoading(false);
          this.app.printError(data.error);
          return;
        }
      } catch (e) {
        this.interactions.toggleLoading(false);
        if (e instanceof Error) console.error(`${e.message || e} [SC main()]`);
        else if (e.stack) console.log(e.stack);
        throw new Error(e.message || e);
      }
    }
    await this.view.update(config.settings, data, state.subplots);
    this.interactions.toggleLoading(false);
  }
  async initPlotComponent(subplotId, opts) {
    const { componentInit: componentInit2 } = await importPlot(opts.chartType);
    this.components.plots[subplotId] = await componentInit2(opts);
  }
  removeComponent(subplotId) {
    this.components.plots[subplotId].destroy();
    delete this.components.plots[subplotId];
  }
};
var SCInit = getCompInit(SCViewer);
var componentInit = SCInit;
function getPlotConfig(opts, app) {
  const config = {
    chartType: "sc",
    hidePlotFilter: true,
    settings: getDefaultSCAppSettings(opts.overrides, app)
  };
  return copyMerge(config, opts);
}
export {
  SCInit,
  SCViewer,
  componentInit,
  getPlotConfig
};
//# sourceMappingURL=SC-M6RGALZM.js.map
