import {
  LegendCircleReference,
  PlotBase,
  appInit,
  axisstyle,
  controlsInit,
  fillTermWrapper,
  fillTwLst,
  table2col,
  to_svg
} 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 {
  termType2label
} 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 {
  roundValue
} from "./chunk-TLT4YIG3.js";
import "./chunk-KYBIQBXE.js";
import {
  select_default
} from "./chunk-I6Y4O3RR.js";
import "./chunk-OMR2DT66.js";
import "./chunk-DQC5FFGV.js";
import "./chunk-HFNDKYVF.js";

// plots/corrVolcano/model/Model.ts
var Model = class {
  constructor(config, state, app, settings, variableTwLst) {
    this.config = config;
    this.state = state;
    this.app = app;
    this.settings = settings;
    this.variableTwLst = variableTwLst;
  }
  async getData() {
    const opts = await this.setRequestOpts();
    const data = await this.app.vocabApi.getCorrelationVolcanoData(opts);
    return data;
  }
  async setRequestOpts() {
    return {
      featureTw: this.config.featureTw,
      variableTwLst: this.variableTwLst,
      filter: this.state.termfilter.filter,
      filter0: this.state.termfilter.filter0,
      correlationMethod: this.settings.method
    };
  }
};

// plots/corrVolcano/viewModel/ViewModel.ts
var ViewModel = class {
  constructor(config, data, dom, settings, variableTwLst) {
    this.bottomPad = 60;
    /** Only one side, left or right */
    this.horizPad = 70;
    this.topPad = 40;
    this.variableTwLst = variableTwLst;
    const pValueKey = settings.isAdjustedPValue ? "adjusted_pvalue" : "original_pvalue";
    const d = this.transformPValues(data, pValueKey);
    const [absYMax, absYMin] = this.setMinMax(d, `transformed_${pValueKey}`);
    const [absXMax, absXMin] = this.setMinMax(d, "correlation");
    const [absSampleMax, absSampleMin] = this.setMinMax(d, "sampleSize");
    const plotDim = this.setPlotDimensions(config, settings, absYMax, absYMin, absXMax, absXMin);
    this.viewData = {
      plotDim,
      variableItems: this.setVariablesData(absSampleMax, absSampleMin, d, dom, pValueKey, plotDim, settings),
      legendData: this.setLegendData(absSampleMin, absSampleMax, data.skippedVariables)
    };
  }
  transformPValues(data, key) {
    data.variableItems = data.variableItems.filter((v) => v[key] > 0);
    for (const item of data.variableItems) {
      if (item[key] > 0) item[`transformed_${key}`] = -Math.log10(item[key]);
      else item[key] = null;
    }
    return data;
  }
  setMinMax(data, key) {
    const sortedValues = data.variableItems.map((v) => v[key]).sort((a, b) => a - b);
    const max = sortedValues[sortedValues.length - 1];
    const min = sortedValues[0];
    return [max, min];
  }
  setPlotDimensions(config, settings, absYMax, absYMin, absXMax, absXMin) {
    const maxXRange = Math.max(Math.abs(absXMin), absXMax);
    const xScale = linear().domain(this.setDomain(-maxXRange, maxXRange, 0.05)).range([0, settings.width]);
    const yScale = linear().domain(this.setDomain(absYMin, absYMax)).range([settings.height, 0]);
    return {
      svg: {
        height: settings.height + this.topPad + this.bottomPad * 2,
        width: settings.width + this.horizPad * 2
      },
      title: {
        text: `${config.featureTw.term.name} ${termType2label(config.featureTw.term.type)}`,
        x: this.horizPad + settings.width / 2,
        y: this.topPad / 2
      },
      xAxisLabel: {
        x: this.horizPad + settings.width / 2,
        y: this.topPad + settings.height + this.bottomPad
      },
      yAxisLabel: {
        x: this.horizPad / 3,
        y: this.topPad + settings.height / 2
      },
      xScale: {
        scale: xScale,
        x: this.horizPad,
        y: settings.height + this.topPad
      },
      yScale: {
        //Do not use scaleLog() here. scaleLog is for raw values before the log transformation
        //Using it will distort the values.
        scale: yScale,
        x: this.horizPad,
        y: this.topPad
      },
      divideLine: {
        x: xScale(0) + this.horizPad,
        y1: settings.height + this.topPad,
        y2: this.topPad
      },
      thresholdLine: {
        y: yScale(-Math.log10(settings.threshold)) + this.topPad,
        x1: this.horizPad,
        x2: settings.width + this.horizPad
      }
    };
  }
  /** Increase the domain slightly so all data points fit within the plot */
  setDomain(min, max, percent = 0.1) {
    const rangeInc = (max - min) * percent;
    const domain = [min - rangeInc, max + rangeInc];
    return domain;
  }
  setVariablesData(absSampleMax, absSampleMin, data, dom, key, plotDim, settings) {
    const radiusScale = linear().domain([absSampleMin, absSampleMax]).range([settings.radiusMin, settings.radiusMax]);
    const renderedCircles = dom.plot?.selectAll("circle").nodes().map((d) => d.__data__);
    for (const item of data.variableItems) {
      item.color = item.correlation > 0 ? settings.corrColor : settings.antiCorrColor;
      item.label = this.variableTwLst.find((t) => t.$id == item.tw$id).term.name;
      item.x = plotDim.xScale.scale(item.correlation) + this.horizPad;
      item.y = plotDim.yScale.scale(item[`transformed_${key}`]) + this.topPad;
      item.radius = radiusScale(item.sampleSize);
      if (renderedCircles?.length > 0) {
        const findRenderdCircle = renderedCircles.find((d) => d.tw$id === item.tw$id);
        if (findRenderdCircle) {
          item.previousX = findRenderdCircle.x;
          item.previousY = findRenderdCircle.y;
        }
      } else {
        item.previousX = item.x;
        item.previousY = item.y;
      }
    }
    return data.variableItems;
  }
  setLegendData(absSampleMin, absSampleMax, skippedVariables) {
    const opts = {
      absMin: absSampleMin,
      absMax: absSampleMax,
      skippedVariables: skippedVariables.length == 0 ? [] : skippedVariables.map((t) => {
        return { label: this.variableTwLst.find((tw) => tw.$id == t.tw$id).term.name };
      })
    };
    return opts;
  }
};

// plots/corrVolcano/view/ItemToolTip.ts
var ItemToolTip = class {
  constructor(item, circle, tip, settings) {
    circle.on("mouseover", () => {
      tip.clear().showunder(circle.node());
      const table = table2col({ holder: tip.d.append("table") });
      table.addRow("Item", item.label);
      table.addRow("Correlation (\u03C1)", roundValue(item.correlation, 3));
      const pValue = settings.isAdjustedPValue ? item.adjusted_pvalue : item.original_pvalue;
      table.addRow(settings.isAdjustedPValue ? "Adjusted p value" : "Original p value", roundValue(pValue, 5));
      table.addRow("Sample size", item.sampleSize);
    });
    circle.on("mouseout", () => {
      tip.hide();
    });
  }
};

// plots/corrVolcano/view/View.ts
var View = class {
  constructor(dom, viewData, interactions, settings, defaultMaxRadius, defaultMinRadius) {
    this.duration = 500;
    this.dom = dom;
    this.viewData = viewData;
    interactions.clearDom();
    const plotDim = viewData.plotDim;
    this.renderDom(plotDim);
    renderVariables(this, settings, interactions);
    this.renderLegend(viewData.legendData, settings, interactions, defaultMaxRadius, defaultMinRadius);
  }
  renderDom(plotDim) {
    this.dom.svg.attr("width", plotDim.svg.width).attr("height", plotDim.svg.height);
    this.dom.title.attr("class", "sjpp-corr-volcano-title").style("font-weight", 600).attr("text-anchor", "middle").attr("x", plotDim.title.x).attr("y", plotDim.title.y).text(plotDim.title.text);
    this.dom.yAxisLabel.attr("class", "sjpp-corr-volcano-y-axis").style("font-weight", 600).attr("text-anchor", "middle").attr("transform", `translate(${plotDim.yAxisLabel.x}, ${plotDim.yAxisLabel.y}) rotate(-90)`).text("-log10(p value)");
    this.dom.xAxisLabel.attr("class", "sjpp-corr-volcano-x-axis").style("font-weight", 600).attr("text-anchor", "middle").attr("transform", `translate(${plotDim.xAxisLabel.x}, ${plotDim.xAxisLabel.y})`).html("Correlation Coefficient (&#961;)");
    this.renderScale(plotDim.yScale, true);
    this.renderScale(plotDim.xScale);
    this.dom.svg.append("line").attr("class", "sjpp-corr-volcano-divide-line").attr("stroke", "black").attr("stroke-dasharray", "5 4").attr("stroke-opacity", 0.4).attr("x1", plotDim.divideLine.x).attr("x2", plotDim.divideLine.x).attr("y1", plotDim.divideLine.y1).attr("y2", plotDim.divideLine.y2);
    this.dom.svg.append("line").attr("class", "sjpp-corr-volcano-threshold-line").attr("stroke", "black").attr("stroke-dasharray", "5 4").attr("stroke-opacity", 0.4).attr("x1", plotDim.thresholdLine.x1).attr("x2", plotDim.thresholdLine.x2).attr("y1", plotDim.thresholdLine.y).attr("y2", plotDim.thresholdLine.y);
  }
  renderScale(scale, isLeft = false) {
    const scaleG = this.dom.plot.append("g").attr("transform", `translate(${scale.x}, ${scale.y})`).transition().duration(this.duration).call(isLeft ? axisLeft(scale.scale) : axisBottom(scale.scale));
    axisstyle({
      axis: scaleG,
      color: "black",
      showline: true
    });
  }
  renderLegend(legendData, settings, interactions, defaultMaxRadius, defaultMinRadius) {
    const svg = this.dom.legend.attr("width", 400).attr("height", 400).style("display", "inline-block").style("vertical-align", "top").style("padding-top", "20px");
    const startXPos = 20;
    const startYPos = 20;
    new LegendCircleReference({
      g: svg.append("g").attr("transform", `translate(${startXPos}, ${startYPos})`),
      inputMax: defaultMaxRadius,
      inputMin: defaultMinRadius,
      maxLabel: legendData.absMax,
      maxRadius: settings.radiusMax,
      minLabel: legendData.absMin,
      minRadius: settings.radiusMin,
      title: "Sample Size",
      menu: {
        minMaxLabel: "pixels",
        callback: async (obj) => {
          await interactions.changeRadius(obj);
        }
      }
    });
    if (legendData.skippedVariables.length > 0) {
      const newYPos = startYPos + settings.radiusMax + 60;
      let incrY = 30;
      const skippedVariablesG = svg.append("g").attr("transform", `translate(${startXPos}, ${newYPos})`);
      skippedVariablesG.append("text").style("font-weight", "bold").style("font-size", "0.8em").text("Skipped Variables");
      for (const term of legendData.skippedVariables) {
        skippedVariablesG.append("text").attr("transform", `translate(${startXPos}, ${incrY})`).style("font-size", "0.8em").text(term.label);
        incrY += 20;
      }
    }
  }
};
function renderVariables(self, settings, interactions) {
  self.dom.plot.selectAll("circle").data(self.viewData.variableItems).enter().append("circle").attr("id", (item) => `sjpp-corr-volcano-circle-${item.label}`).attr("stroke", (item) => item.color).attr("fill", (item) => item.color).attr("fill-opacity", 0.5).attr("cx", (item) => item.previousX).attr("cy", (item) => item.previousY).attr("r", (item) => item.radius * 0.9).transition().duration(self.duration).attr("cx", (item) => item.x).attr("cy", (item) => item.y).attr("r", (item) => item.radius).each(function(item) {
    const circle = select_default(this);
    new ItemToolTip(item, circle, self.dom.tip, settings);
    circle.on("click", () => {
      interactions.launchSampleScatter(item);
    });
  });
}

// plots/corrVolcano/interactions/CorrVolcanoInteractions.ts
var CorrVolcanoInteractions = class {
  constructor(app, dom, id) {
    this.app = app;
    this.dom = dom;
    this.id = id;
    this.variableTwLst = [];
  }
  download() {
    const svg = this.dom.svg.node();
    to_svg(svg, `correlationVolcano`, { apply_dom_styles: true });
  }
  //If no featureTw is set, show the tree to select a feature
  async showTree() {
    this.dom.div.selectAll("*").remove();
    await appInit({
      vocabApi: this.app.vocabApi,
      holder: this.dom.div,
      state: this.app.getState(),
      tree: {
        click_term: (_term) => {
          const term = _term.term || _term;
          this.app.dispatch({
            type: "plot_create",
            config: {
              chartType: "correlationVolcano",
              featureTw: _term.term ? _term : { term }
            }
          });
          this.app.dispatch({
            type: "plot_delete",
            id: this.id
          });
        }
      }
    });
  }
  //When clicking on dot, launch the sample scatter by gene and drug
  launchSampleScatter(item) {
    const config = this.app.getState();
    const plot = config.plots.find((p) => p.id === this.id);
    const term2 = this.variableTwLst?.find((t) => t.$id === item.tw$id)?.term;
    if (!term2) throw `No term found for ${item.tw$id}`;
    const scatterConfig = {
      chartType: "sampleScatter",
      name: `${plot.featureTw.term.name} ${termType2label(plot.featureTw.term.type)} v ${term2.name}`,
      term: { term: plot.featureTw.term },
      term2: { id: term2.id },
      filter: config.termfilter.filter
    };
    this.app.dispatch({
      type: "plot_create",
      config: scatterConfig
    });
  }
  //Obj is returned from LegendCircleReference callback
  async changeRadius(obj) {
    this.app.dispatch({
      type: "plot_edit",
      id: this.id,
      config: {
        settings: {
          correlationVolcano: {
            radiusMax: obj.max,
            radiusMin: obj.min
          }
        }
      }
    });
  }
  clearDom() {
    this.dom.error.style("padding", "").text("");
    this.dom.plot.selectAll("*").remove();
    this.dom.legend.selectAll("*").remove();
    this.dom.svg.selectAll("line").remove();
    this.dom.title.text("");
    this.dom.yAxisLabel.text("");
    this.dom.xAxisLabel.text("");
  }
};

// plots/corrVolcano/CorrelationVolcano.ts
var CorrelationVolcano = class extends PlotBase {
  constructor(opts, api) {
    super(opts, api);
    this.type = "correlationVolcano";
    /** Max radius user may enter in control or legend menu */
    this.defaultInputMaxRadius = 35;
    /** Min radius user may enter in control or legend menu */
    this.defaultInputMinRadius = 1;
    this.opts = opts;
    this.components = {
      controls: {}
    };
    const holder = opts.holder.classed("sjpp-corrVolcano-main", true);
    const controls = opts.controls ? holder : holder.append("div");
    const div = holder.append("div").style("padding", "5px").style("display", "inline-block");
    const errorDiv = div.append("div").attr("id", "sjpp-corrVolcano-error").style("opacity", 0.75);
    const svg = div.append("svg").style("display", "inline-block").attr("id", "sjpp-corrVolcano-svg");
    this.dom = {
      controls: controls.style("display", "block"),
      div,
      error: errorDiv,
      svg,
      plot: svg.append("g"),
      title: svg.append("text"),
      yAxisLabel: svg.append("text"),
      xAxisLabel: svg.append("text"),
      legend: div.append("svg"),
      tip: new Menu({ padding: "" })
    };
    if (opts.header)
      this.dom.header = opts.header.text("CORRELATION VOLCANO").style("font-size", "0.7em").style("opacity", 0.6);
    this.variableTwLst = [];
  }
  static {
    this.type = "correlationVolcano";
  }
  getState(appState) {
    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)?`;
    }
    return {
      termdbConfig: appState.termdbConfig,
      termfilter: appState.termfilter,
      correlationVolcano: appState.termdbConfig.correlationVolcano,
      config: Object.assign({}, config, {
        settings: {
          correlationVolcano: config.settings.correlationVolcano
        }
      })
    };
  }
  async setControls() {
    const inputs = [
      {
        type: "term",
        configKey: "featureTw",
        chartType: "correlationVolcano",
        usecase: { target: "correlationVolcano", detail: "numeric" },
        label: "Feature",
        vocabApi: this.app.vocabApi,
        numericEditMenuVersion: ["continuous"],
        menuOptions: "replace"
      },
      {
        label: "Correlation method",
        title: "Change the correlation method",
        type: "radio",
        chartType: "correlationVolcano",
        settingsKey: "method",
        options: [
          { label: "Pearson", value: "pearson" },
          { label: "Spearman", value: "spearman" }
        ]
      },
      {
        label: "P value",
        title: "Change the p value",
        type: "radio",
        chartType: "correlationVolcano",
        settingsKey: "isAdjustedPValue",
        options: [
          { label: "Adjusted", value: true },
          { label: "Original", value: false }
        ]
      },
      {
        label: "Significant p value",
        title: "Set the significant p value threshold",
        type: "number",
        chartType: "correlationVolcano",
        settingsKey: "threshold",
        debounceInterval: 0.05
      },
      {
        label: "Maximum radius size",
        title: "Set maximum radius size in pixels",
        type: "number",
        chartType: "correlationVolcano",
        settingsKey: "radiusMax",
        min: this.defaultInputMinRadius,
        max: this.defaultInputMaxRadius,
        debounceInterval: 500
      },
      {
        label: "Minimum radius size",
        title: "Set minimum radius size in pixels",
        type: "number",
        chartType: "correlationVolcano",
        settingsKey: "radiusMin",
        min: this.defaultInputMinRadius,
        max: this.defaultInputMaxRadius,
        debounceInterval: 500
      },
      {
        label: "Height",
        title: "Set the height of the plot",
        type: "number",
        chartType: "correlationVolcano",
        settingsKey: "height",
        debounceInterval: 500
      },
      {
        label: "Width",
        title: "Set the width of the plot",
        type: "number",
        chartType: "correlationVolcano",
        settingsKey: "width",
        debounceInterval: 500
      },
      {
        label: "Correlation color",
        type: "color",
        chartType: "correlationVolcano",
        settingsKey: "corrColor",
        title: "Color of correlated values"
      },
      {
        label: "Anticorrelation color",
        type: "color",
        chartType: "correlationVolcano",
        settingsKey: "antiCorrColor",
        title: "Color of anticorrelated values"
      }
    ];
    this.components.controls = await controlsInit({
      app: this.app,
      id: this.id,
      holder: this.dom.controls.style("display", "inline-block"),
      inputs
    });
    this.components.controls.on("downloadClick.correlationVolcano", () => {
      this.interactions.download();
    });
    this.components.controls.on("helpClick.correlationVolcano", () => {
      window.open("https://github.com/stjude/proteinpaint/wiki/Correlation-volcano", "_blank");
    });
  }
  async init(appState) {
    this.interactions = new CorrVolcanoInteractions(this.app, this.dom, this.id);
    const state = this.getState(appState);
    if (state.config["featureTw"]) await this.setControls();
    const dsCorrVolcano = state.termdbConfig.correlationVolcano;
    this.variableTwLst = dsCorrVolcano.variables.termIds.map((id) => {
      return { id };
    });
    this.interactions.variableTwLst = this.variableTwLst;
    await fillTwLst(this.variableTwLst, this.app.vocabApi);
  }
  async main() {
    const config = structuredClone(this.state.config);
    if (config.childType != this.type && config.chartType != this.type) return;
    if (!this.interactions) throw "Interactions not initialized [correlationVolcano main()]";
    if (config.featureTw == null) {
      this.interactions.showTree();
      return;
    }
    const settings = config.settings.correlationVolcano;
    const model = new Model(config, this.state, this.app, settings, this.variableTwLst);
    const data = await model.getData();
    if (!data || data.error || !data.variableItems.length) {
      this.interactions.clearDom();
      this.dom.error.style("padding", "20px 20px 20px 60px").text(data.error || "No correlation data to render.");
      return;
    }
    const viewModel = new ViewModel(config, data, this.dom, settings, this.variableTwLst);
    new View(
      this.dom,
      viewModel.viewData,
      this.interactions,
      settings,
      this.defaultInputMaxRadius,
      this.defaultInputMinRadius
    );
  }
};
var corrVolcanoInit = getCompInit(CorrelationVolcano);
var componentInit = corrVolcanoInit;
function getDefaultCorrVolcanoSettings(overrides = {}) {
  const defaults = {
    antiCorrColor: "#ff0000",
    //red
    corrColor: "#0000ff",
    //blue
    isAdjustedPValue: false,
    method: "pearson",
    threshold: 0.05,
    height: 500,
    width: 500,
    radiusMax: 20,
    radiusMin: 5
  };
  return Object.assign(defaults, overrides);
}
async function getPlotConfig(opts, app) {
  if (opts.numeric && !opts.featureTw) opts.featureTw = { term: opts.numeric.term };
  if (opts.featureTw) {
    try {
      await fillTermWrapper(opts.featureTw, app.vocabApi);
    } catch (e) {
      console.error(new Error(`${e} [correlationVolcano getPlotConfig()]`));
      throw `correlationVolcano getPlotConfig() failed`;
    }
  }
  const config = {
    chartType: "correlationVolcano",
    featureTw: opts.featureTw ?? null,
    settings: {
      controls: {
        term2: null,
        term0: null
      },
      correlationVolcano: getDefaultCorrVolcanoSettings(opts.overrides || {})
    }
  };
  return copyMerge(config, opts);
}
export {
  componentInit,
  corrVolcanoInit,
  getDefaultCorrVolcanoSettings,
  getPlotConfig
};
//# sourceMappingURL=CorrelationVolcano-RO6CFLZA.js.map
