SummarizedExperiment.js

import { Matrix } from "./Matrix.js";
import { DataFrame } from "./DataFrame.js";
import { ReducedDimensionResult } from "./ReducedDimensionResult.js";

/**
 * An interface to the **wobbegong** representation of a SummarizedExperiment instance.
 */
export class SummarizedExperiment {
    #summary;
    #path;
    #fetch_json;
    #fetch_range;

    /**
     * @param {object} summary - A summary of the SummarizedExperiment contents, according to the **wobbegong** specifications.
     * @param {string} path - Path to the SummarizedExperiment directory.
     * This may be a relative or absolute path, depending on how the files are hosted.
     * @param {function} fetch_json - A function that accepts `path`, a path to a file inside `path`.
     * It should retrieve the contents of `path` and load them as a JSON object.
     * It may also return a promise that resolves to such an object.
     * @param {function} fetch_range - A function that accepts `file` (a path to a file inside `path`), `start` and `end`.
     * It should retrieve bytes from `path` in the interval `[start, end)` and return a Uint8Array containing those bytes.
     * It may also return a promise that resolves to such a Uint8Array.
     */
    constructor(summary, path, fetch_json, fetch_range) {
        this.#summary = summary;
        this.#path = path;
        this.#fetch_json = fetch_json;
        this.#fetch_range = fetch_range;
    }

    /**
     * @return {number} Number of rows.
     */
    numberOfRows() {
        return this.#summary.row_count;
    }

    /**
     * @return {number} Number of columns.
     */
    numberOfColumns() {
        return this.#summary.column_count;
    }

    /**
     * @return {boolean} Whether row data is available.
     */
    hasRowData() {
        return this.#summary.has_row_data;
    }

    /**
     * @return {boolean} Whether column data is available.
     */
    hasColumnData() {
        return this.#summary.has_column_data;
    }

    /**
     * @return {?DataFrame} A {@link DataFrame} instance representing the row data,
     * or `null` if {@linkcode SummarizedExperiment#hasRowData hasRowData} is false.
     */
    async rowData() {
        if (!this.hasRowData()) {
            return null;
        }
        let path = this.#path + "/row_data";
        const summary = await this.#fetch_json(path + "/summary.json");
        return new DataFrame(summary, this.#path + "/row_data", this.#fetch_range);
    }

    /**
     * @return {?DataFrame} A {@link DataFrame} instance representing the column data,
     * or `null` if {@linkcode SummarizedExperiment#hasColumnData hasColumnData} is false.
     */
    async columnData() {
        if (!this.hasColumnData()) {
            return null;
        }
        let path = this.#path + "/column_data";
        const summary = await this.#fetch_json(path + "/summary.json");
        return new DataFrame(summary, path, this.#fetch_range);
    }

    /**
     * @return {Array} Names of the assays.
     */
    assayNames() {
        return this.#summary.assay_names;
    }

    /**
     * @param {number|string} i - Index or name of the assay to retrieve.
     * @return {Matrix} A {@link Matrix} instance representing the assay matrix `i`.
     */
    async assay(i) {
        if (typeof i === "string") {
            i = this.#summary.assay_names.indexOf(i);
            if (i === -1) {
                throw new Error("could not find assay named '" + i + "'");
            }
        }
        let path = this.#path + "/assays/" + String(i); 
        const summary = await this.#fetch_json(path + "/summary.json");
        return new Matrix(summary, path, this.#fetch_range);
    }

    /**
     * @return {boolean} Whether the underlying object is a SingleCellExperiment.
     */
    isSingleCellExperiment() {
        return this.#summary.object == "single_cell_experiment";
    }

    /**
     * @return {?Array} Names of the reduced dimensions.
     * Alternatively `null` if {@linkcode SummarizedExperiment#isSingleCellExperiment isSingleCellExperiment} is false.
     */
    reducedDimensionNames() {
        if (!this.isSingleCellExperiment()) {
            return null;
        }
        return this.#summary.reduced_dimension_names;
    }

    /**
     * @param {number|string} i - Index or name of the reduced dimension result to retrieve.
     * @return {?ReducedDimensionResult} A {@link ReducedDimensionResult} instance representing the reduced dimension result `i`,
     * or `null` if {@linkcode SummarizedExperiment#isSingleCellExperiment isSingleCellExperiment} is false.
     */
    async reducedDimension(i) {
        if (!this.isSingleCellExperiment()) {
            return null;
        }
        if (typeof i === "string") {
            i = this.#summary.reduced_dimension_names.indexOf(i);
            if (i === -1) {
                throw new Error("could not find reduced dimension result named '" + i + "'");
            }
        }
        let path = this.#path + "/reduced_dimensions/" + String(i); 
        const summary = await this.#fetch_json(path + "/summary.json");
        return new ReducedDimensionResult(summary, path, this.#fetch_range);
    }

    /**
     * @return {?Array} Names of the alternative experiments.
     * Alternatively `null` if {@linkcode SummarizedExperiment#isSingleCellExperiment isSingleCellExperiment} is false.
     */
    alternativeExperimentNames() {
        if (!this.isSingleCellExperiment()) {
            return null;
        }
        return this.#summary.alternative_experiment_names;
    }

    /**
     * @param {number|string} i - Index or name of the alternative experiment to retrieve.
     * @return {?SummarizedExperiment} A {@link SummarizedExperiment} instance representing the alternative experiment `i`,
     * or `null` if {@linkcode SummarizedExperiment#isSingleCellExperiment isSingleCellExperiment} is false.
     */
    async alternativeExperiment(i) {
        if (!this.isSingleCellExperiment()) {
            return null;
        }
        if (typeof i === "string") {
            i = this.#summary.alternative_experiment_names.indexOf(i);
            if (i === -1) {
                throw new Error("could not find alternative experiment named '" + i + "'");
            }
        }
        let path = this.#path + "/alternative_experiments/" + String(i); 
        const summary = await this.#fetch_json(path + "/summary.json");
        return new SummarizedExperiment(summary, path, this.#fetch_json, this.#fetch_range);
    }
}