import {
  setViolinRenderer
} from "./chunk-DR6H3QAA.js";
import {
  setInteractivity
} from "./chunk-MYF2BWKC.js";
import {
  PlotBase,
  controlsInit,
  defaultUiLabels,
  fillTermWrapper,
  getCombinedTermFilter,
  htmlLegend,
  renderTerm1Label,
  term0_term2_defaultQ
} from "./chunk-SA7APTJR.js";
import {
  Menu
} from "./chunk-HYOEWQ5P.js";
import {
  copyMerge,
  getCompInit
} from "./chunk-M3J4MINX.js";
import {
  isNumericTerm,
  isSingleCellTerm
} from "./chunk-NOEAT6CX.js";
import {
  plotColor
} from "./chunk-4UWS5Y3N.js";

// plots/violin.js
var ViolinPlot = class _ViolinPlot extends PlotBase {
  static type = "violin";
  constructor(opts) {
    super(opts);
    this.type = _ViolinPlot.type;
  }
  async init(appState) {
    const config = appState.plots.find((p) => p.id === this.id);
    const holder = this.opts.holder.append("div").attr("id", "sjpp-vp-holder");
    const { controls, errdiv, loadingDiv, banner, renderedData, charts, legendDiv } = this.getStandardDomLayout(
      holder,
      this.opts
    );
    this.dom = {
      hovertip: new Menu({ padding: "3px" }),
      // separate tips for hover & click on violin labels to avoid interfering
      clicktip: new Menu({ padding: "0px" }),
      sampletabletip: new Menu({ padding: "3px" }),
      // sampletable is lauched from option shown in clicktip which closes on clicking, thus need its own menu..
      header: this.opts.header,
      controls: controls.attr("class", "sjpp-plot-controls").style("display", "inline-block"),
      banner: banner.attr("data-testid", "sjpp-violin-banner"),
      errdiv,
      loadingDiv: loadingDiv.attr("data-testid", "sjpp-violin-loading-div").style("display", this.opts.mode != "minimal" ? "inline-block" : "none"),
      renderedData,
      violinDiv: charts.append("div").attr("class", "sjpp-vp-violinDiv").attr("data-testid", "sjpp-violin-div").style("display", "flex").style("flex-direction", "row").style("flex-wrap", "wrap").style("max-width", "100vw"),
      legendDiv: legendDiv.classed("sjpp-vp-legend", true).attr("data-testid", "sjpp-violin-legend").style("margin-left", "-15px").style("padding-top", "20px")
    };
    setViolinRenderer(this);
    setInteractivity(this);
    if (this.opts.mode != "minimal") {
      this.legendRenderer = htmlLegend(this.dom.legendDiv, {
        settings: {
          legendOrientation: "vertical"
        },
        handlers: {}
      });
    }
  }
  reactsTo(action) {
    if (action.type.startsWith("plot_")) {
      return (action.id === this.id || action.id == this.parentId) && (!action.config?.childType || action.config?.childType == this.type);
    }
    return true;
  }
  preApiFreeze(api) {
    api.download = this.download;
    api.getChartImages = () => this.getChartImages();
  }
  async setControls() {
    if (this.components.controls) return;
    this.dom.controls.selectAll("*").remove();
    const controlLabels = this.state.config.controlLabels;
    if (!controlLabels) throw "controls labels not found";
    this.components = {};
    if (this.opts.mode == "minimal") return;
    let specialCase = "default";
    if (isSingleCellTerm(this.config.term.term)) {
      if (!this.config.term.term.sample)
        console.error("single cell term without sample specified in config, unexpected");
      else {
        specialCase = {
          type: "singleCell",
          config: { sample: this.config.term.term.sample }
        };
      }
    }
    const inputs = [
      {
        type: "term",
        configKey: "term",
        chartType: "violin",
        usecase: { target: "violin", detail: "term" },
        label: controlLabels.term1?.label || renderTerm1Label,
        vocabApi: this.app.vocabApi,
        menuOptions: "edit"
      },
      {
        type: "term",
        configKey: "term2",
        chartType: "violin",
        usecase: { target: "violin", detail: "term2", specialCase },
        title: controlLabels.term2.title || controlLabels.term2.label,
        label: controlLabels.term2.label,
        vocabApi: this.app.vocabApi,
        numericEditMenuVersion: this.opts.numericEditMenuVersion,
        defaultQ4fillTW: term0_term2_defaultQ,
        getDisplayStyle: (plot) => plot.config?.term?.term?.type === "termCollection" ? "none" : ""
      },
      {
        type: "term",
        configKey: "term0",
        chartType: "violin",
        usecase: { target: "violin", detail: "term0", specialCase },
        title: controlLabels.term0.title || controlLabels.term0.label,
        label: controlLabels.term0.label,
        vocabApi: this.app.vocabApi,
        // by default, do not allow continuous mode for divide-by term, since
        // it will create a separate violin-overlay group per unique float or integer value
        // and there will nonsensical tens/hundreds of these charts based on the cohort size
        numericEditMenuVersion: this.opts.numericEditMenuVersion || ["discrete"],
        defaultQ4fillTW: term0_term2_defaultQ,
        getDisplayStyle: (plot) => plot.config?.term?.term?.type === "termCollection" ? "none" : ""
      },
      {
        label: "Orientation",
        title: "Orientation of the chart",
        type: "radio",
        chartType: "violin",
        settingsKey: "orientation",
        options: [
          { label: "Vertical", value: "vertical" },
          { label: "Horizontal", value: "horizontal" }
        ]
      },
      {
        label: "Data symbol",
        title: "Symbol type",
        type: "radio",
        chartType: "violin",
        settingsKey: "datasymbol",
        options: [
          { label: "Ticks", value: "rug" },
          { label: "Circles", value: "bean" },
          { label: "Off", value: "none" }
        ]
      },
      {
        label: "Scale",
        title: "Axis scale",
        type: "radio",
        chartType: "violin",
        settingsKey: "isLogScale",
        options: [
          { label: "Linear", value: false },
          { label: "Log10", value: true }
        ]
      },
      {
        label: "Order by",
        title: "Order violin plots by parameters",
        type: "radio",
        chartType: "violin",
        settingsKey: "orderByMedian",
        options: [
          { label: "Default", value: false },
          { label: "Median", value: true }
        ],
        getDisplayStyle: () => {
          let style = "none";
          const charts = this.data?.charts || {};
          for (const k of Object.keys(charts)) {
            const chart = charts[k];
            if (chart?.plots.length > 1) style = "";
          }
          return style;
        }
      },
      {
        label: "Symbol size",
        type: "number",
        chartType: "violin",
        settingsKey: "radius",
        step: 1,
        max: 30,
        min: 4
      },
      {
        label: "Bins",
        type: "number",
        chartType: "violin",
        settingsKey: "ticks",
        title: "Number of bins used to build the plot",
        min: 1,
        max: 50
      },
      {
        label: "Plot length",
        title: "Length of the plot",
        type: "number",
        chartType: "violin",
        settingsKey: "svgw",
        step: 10,
        max: 1e3,
        min: 200,
        debounceInterval: 1e3
      },
      {
        label: "Plot thickness",
        title: "If not specified, the plot thickness is calculated based on the number of categories",
        type: "number",
        chartType: "violin",
        settingsKey: "plotThickness",
        step: 10,
        max: 200,
        min: 20,
        debounceInterval: 1e3
      },
      {
        label: "Plot padding",
        type: "number",
        chartType: "violin",
        settingsKey: "rowSpace",
        step: 1,
        max: 20,
        min: 0,
        debounceInterval: 1e3
      },
      {
        label: "Median length",
        title: "Length of median",
        type: "number",
        chartType: "violin",
        settingsKey: "medianLength",
        step: 1,
        max: 50,
        min: 3,
        debounceInterval: 1e3
      },
      {
        label: "Median color",
        title: "color of median",
        type: "color",
        chartType: "violin",
        settingsKey: "medianColor"
      },
      {
        label: "Median thickness",
        title: "Width of median",
        type: "number",
        chartType: "violin",
        settingsKey: "medianThickness",
        step: 1,
        max: 10,
        min: 3,
        debounceInterval: 100
      },
      {
        label: "Default violin color",
        type: "color",
        chartType: "violin",
        settingsKey: "defaultColor"
      },
      {
        label: "Show stats",
        type: "checkbox",
        chartType: "violin",
        settingsKey: "showStats",
        boxLabel: "Yes"
      },
      {
        label: "Show association tests",
        type: "checkbox",
        chartType: "violin",
        settingsKey: "showAssociationTests",
        boxLabel: "Yes",
        getDisplayStyle: (plot) => plot.term2 ? "table-row" : "none"
      }
    ];
    this.components.controls = await controlsInit({
      app: this.app,
      id: this.id,
      holder: this.dom.controls,
      inputs
    });
    this.components.controls.on("downloadClick.violin", this.download);
  }
  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)?`;
    }
    const parentConfig = appState.plots.find((p) => p.id === this.parentId);
    const termfilter = getCombinedTermFilter(appState, config.filter || parentConfig?.filter);
    return {
      termfilter,
      config,
      displaySampleIds: appState.termdbConfig.displaySampleIds,
      hasVerifiedToken: this.app.vocabApi.hasVerifiedToken()
    };
  }
  async main() {
    this.config = structuredClone(this.state.config);
    this.settings = this.config.settings.violin;
    await this.setControls();
    if (this.config.chartType != this.type && this.config.childType != this.type) return;
    if (this.dom.header)
      this.dom.header.html(
        this.config.term.term.name + ` <span style="opacity:.6;font-size:1em;margin-left:10px;">Violin Plot</span>`
      );
    const existingMsg = this.dom.banner.style("display", "none").select("span");
    if (!existingMsg.empty()) existingMsg.remove();
    const args = this.validateArgs();
    let data;
    try {
      this.toggleLoadingDiv("block", "block");
      data = await this.app.vocabApi.getViolinBox(args, null, this.api.getAbortSignal());
    } catch (e) {
      this.toggleLoadingDiv("none", "none");
      if (this.app.isAbortError(e)) return;
      throw e;
    }
    this.data = data;
    if (this.data.error) {
      this.toggleLoadingDiv("none", "none");
      throw this.data.error;
    }
    args.tw.q.descrStats = this.data.descrStats;
    setTimeout(
      () => {
        this.render();
        this.toggleLoadingDiv("none");
      },
      this.opts.mode == "minimal" ? 0 : 500
    );
  }
  validateArgs() {
    const { term, term2, term0, settings } = this.config;
    const s = this.settings;
    const arg = {
      plotType: "violin",
      svgw: s.svgw,
      orientation: s.orientation,
      devicePixelRatio: window.devicePixelRatio,
      datasymbol: s.datasymbol,
      radius: s.radius,
      axisHeight: s.axisHeight,
      rightMargin: s.rightMargin,
      isLogScale: s.isLogScale,
      ticks: s.ticks,
      orderByMedian: s.orderByMedian,
      filter: this.state.termfilter.filter
    };
    if (this.opts.mode == "minimal") {
      arg.tw = term;
      if (term2 || term0) throw "only a single term allowed for minimal plot";
      if (term.q.mode == "spline") {
        s.lines = term.q.knots.map((x) => Number(x.value));
      } else {
        s.lines = [];
      }
      if (term.q.scale) {
        arg.scale = term.q.scale;
      }
    } else if (term.term?.type === "termCollection" && term.term?.memberType === "numeric") {
      arg.tw = term;
    } else if (isNumericTerm(term.term) && term.q.mode === "continuous") {
      arg.tw = term;
      if (term2) arg.overlayTw = term2;
    } else if (isNumericTerm(term2?.term) && term2.q.mode === "continuous") {
      arg.tw = term2;
      arg.overlayTw = term;
    } else {
      throw "both term1 and term2 are not numeric/continuous";
    }
    if (term0) arg.divideTw = term0;
    return arg;
  }
};
var violinInit = getCompInit(ViolinPlot);
var componentInit = violinInit;
function getDefaultViolinSettings(app, overrides = {}) {
  const defaults = {
    orientation: "horizontal",
    rowlabelw: 250,
    brushRange: null,
    //object with start and end if there is a brush selection
    svgw: 500,
    // span length of a plot/svg, not including margin
    datasymbol: "rug",
    radius: 10,
    axisHeight: 60,
    rightMargin: 50,
    lines: [],
    isLogScale: false,
    // false: linear scale, true: log scale
    rowSpace: 10,
    medianLength: 7,
    medianColor: "#FF0000",
    medianThickness: 3,
    ticks: 15,
    defaultColor: plotColor,
    method: 0,
    orderByMedian: false,
    showStats: true,
    showAssociationTests: true
  };
  return Object.assign(defaults, overrides);
}
async function getPlotConfig(opts, app) {
  if (!opts.term) throw "violin getPlotConfig: opts.term{} missing";
  try {
    await fillTermWrapper(opts.term, app.vocabApi);
    if (opts.term2) await fillTermWrapper(opts.term2, app.vocabApi);
    if (opts.term0) await fillTermWrapper(opts.term0, app.vocabApi);
  } catch (e) {
    throw `${e} [violin getPlotConfig()]`;
  }
  const config = {
    controlLabels: Object.assign({}, defaultUiLabels, app.vocabApi.termdbConfig.uiLabels || {}),
    settings: {
      controls: {
        term2: null,
        // the previous overlay value may be displayed as a convenience for toggling
        term0: null
      },
      // common: {
      // 	use_logscale: false, // flag for y-axis scale type, 0=linear, 1=log
      // 	use_percentage: false,
      // 	barheight: 300, // maximum bar length
      // 	barwidth: 20, // bar thickness
      // 	barspace: 2 // space between two bars
      // },
      violin: getDefaultViolinSettings(app)
    }
  };
  return copyMerge(config, opts);
}

export {
  violinInit,
  componentInit,
  getDefaultViolinSettings,
  getPlotConfig
};
//# sourceMappingURL=chunk-FFEIP5CT.js.map
