BioAnalyticResource/ePlant_Plant_eFP

View on GitHub
tissueExpressionBAR.js

Summary

Maintainability
A
0 mins
Test Coverage
/* eslint-disable prefer-destructuring */
//= =========================== Alexander Sullivan =============================
//
// Purpose: Generates eFP tissue expression data
//
//= ============================================================================
/** Stroke data of compendiums that have already been called */
const existingStrokeData = {};
window.existingStrokeData = existingStrokeData;
/**
 * Add details to an SVG or SVG-subunit including: hover and outline
 * @param {String} elementID Which SVG or SVG-subunit is being found and edited
 */
function addTissueMetadata(elementID) {
    // Adjusting for BioticStressPseudomonassyringae's half leaf:
    if (elementID.includes("Half_Leaf_Pseudomonas_syringae")) {
        elementID += "_outline";
    }
    // Retrieve document objects:
    let svgDoc;
    let svgPart;
    let svgPartChildren;
    if (document.getElementById(createSVGExpressionData.svgObjectName)) {
        svgDoc = document.getElementById(createSVGExpressionData.svgObjectName);
        svgPart = svgDoc.getElementById(elementID);
        svgPartChildren = svgPart.childNodes;
    }

    /** Increase stroke width within SVG by (multiplied) this much */
    const increaseStrokeWidthBy = 2.25;
    // Storing stroke widths
    let existingStrokeWidth;
    let existingStrokeColour;

    if (svgDoc && svgPart) {
        if (svgPart.getAttribute("stroke-width")) {
            existingStrokeWidth = svgPart.getAttribute("stroke-width");

            if (svgPart.getAttribute("stroke")) {
                existingStrokeColour = svgPart.getAttribute("stroke");
            }
        } else if (svgPartChildren.length > 0) {
            for (const svgChildPart of svgPartChildren) {
                if (svgChildPart.nodeName === "path") {
                    if (svgChildPart.getAttribute("stroke-width")) {
                        existingStrokeWidth = svgChildPart.getAttribute("stroke-width");
                    }

                    if (svgChildPart.getAttribute("stroke")) {
                        existingStrokeColour = svgChildPart.getAttribute("stroke");
                    }
                }
            }
        }

        if (!existingStrokeData[elementID]) {
            existingStrokeData[elementID] = {};
            existingStrokeData[elementID].strokeWidth = existingStrokeWidth;
            existingStrokeData[elementID].strokeColour = existingStrokeColour;
            existingStrokeData[elementID].addedMetadata = false;
        }

        // Making stroke width thicker
        if (svgDoc.getElementById(elementID) && !existingStrokeData[elementID].addedMetadata) {
            existingStrokeData[elementID].addedMetadata = true;

            const strokeElement = svgDoc.getElementById(elementID);

            // Create hover title box
            if (strokeElement.getBoundingClientRect()) {
                /** Title box's text */
                const titleText = svgPart.getElementsByTagName("title")?.[0]?.textContent;
                /** Title box's x-coordinate */
                const boxLeft = strokeElement.getBoundingClientRect().right;
                /** Title box's y-coordinate */
                const boxTop = strokeElement.getBoundingClientRect().bottom;

                // If all the data is available, add the title box
                if (titleText && boxLeft && boxTop) {
                    ePlantPlantEFPChangeTitlePosition(true, boxLeft, boxTop, titleText);
                } else {
                    // Fail-safe, hide title box
                    ePlantPlantEFPChangeTitlePosition(false);
                }
            } else {
                // Fail-safe, hide title box
                ePlantPlantEFPChangeTitlePosition(false);
            }

            existingStrokeWidth = Number(existingStrokeWidth);
            let newStrokeWidth = existingStrokeWidth * increaseStrokeWidthBy;
            const maxStrokeWidth = increaseStrokeWidthBy;
            const minStrokeWidth = increaseStrokeWidthBy / 2;

            if (newStrokeWidth > maxStrokeWidth && maxStrokeWidth > existingStrokeWidth) {
                newStrokeWidth = maxStrokeWidth;
            } else if (newStrokeWidth < minStrokeWidth && minStrokeWidth > existingStrokeWidth) {
                newStrokeWidth = minStrokeWidth;
            } else if (newStrokeWidth === 0) {
                newStrokeWidth = increaseStrokeWidthBy;
            } else if (!newStrokeWidth) {
                newStrokeWidth = minStrokeWidth;
            }

            /** Boolean to determine if metadata has been added already */
            let addedHoverMetadata = false;
            if (strokeElement.getAttribute("stroke-width")) {
                svgDoc.getElementById(elementID).setAttribute("stroke-width", newStrokeWidth);
                svgDoc.getElementById(elementID).setAttribute("stroke", "#000");

                addedHoverMetadata = true;
            } else if (svgPartChildren && svgPartChildren.length > 0) {
                for (const svgChildPart of svgPartChildren) {
                    if (svgChildPart.nodeName === "path" && svgChildPart.getAttribute("stroke-width")) {
                        svgChildPart.setAttribute("stroke-width", newStrokeWidth);

                        if (svgChildPart.getAttribute("stroke")) {
                            svgChildPart.setAttribute("stroke", "#000");
                        }

                        addedHoverMetadata = true;
                    }
                }
            }

            if (!addedHoverMetadata) {
                svgDoc.getElementById(elementID).setAttribute("stroke-width", newStrokeWidth);
                svgDoc.getElementById(elementID).setAttribute("stroke", "#000");
            }
        }
    }
}

/**
 * Remove details to an SVG or SVG-subunit including: hover and outline
 * @param {String} elementID Which SVG or SVG-subunit is being found and edited
 */
function removeTissueMetadata(elementID) {
    // Adjusting for BioticStressPseudomonassyringae's half leaf:
    if (elementID.includes("Half_Leaf_Pseudomonas_syringae")) {
        elementID += "_outline";
    }

    /** A fallback stroke width for the SVG if one is not already pre-determined */
    const fallbackStrokeWidth = 1;
    /** A fallback stroke colour (black) for the SVG if one is not already pre-determined */
    const fallbackStrokeColour = "#000"; // Black
    // Retrieve document objects:
    let svgDoc;
    let svgPart;
    let svgPartChildren;
    if (document.getElementById(createSVGExpressionData.svgObjectName)) {
        svgDoc = document.getElementById(createSVGExpressionData.svgObjectName);
        svgPart = svgDoc.getElementById(elementID);
        svgPartChildren = svgPart.childNodes;
    }

    // If existing stroke data exists, then proceed
    if (existingStrokeData[elementID] && existingStrokeData[elementID].addedMetadata) {
        existingStrokeData[elementID].addedMetadata = false;
        if (svgPart && svgPart.getAttribute("stroke-width")) {
            if (Number(existingStrokeData[elementID].strokeWidth) >= 0) {
                svgDoc
                    .getElementById(elementID)
                    .setAttribute("stroke-width", existingStrokeData[elementID].strokeWidth);
            } else if (!existingStrokeData[elementID].strokeWidth) {
                svgDoc.getElementById(elementID).removeAttribute("stroke-width");
            } else {
                svgDoc.getElementById(elementID).setAttribute("stroke-width", fallbackStrokeWidth);
            }

            if (svgPart.getAttribute("stroke")) {
                if (existingStrokeData[elementID].strokeColour) {
                    svgDoc.getElementById(elementID).setAttribute("stroke", existingStrokeData[elementID].strokeColour);
                } else {
                    svgDoc.getElementById(elementID).setAttribute("stroke", fallbackStrokeColour);
                }
            }
        } else if (svgPartChildren.length > 0) {
            for (const svgChildPart of svgPartChildren) {
                if (svgChildPart.nodeName === "path") {
                    if (svgChildPart.getAttribute("stroke-width")) {
                        if (Number(existingStrokeData[elementID].strokeWidth) >= 0) {
                            svgChildPart.setAttribute("stroke-width", existingStrokeData[elementID].strokeWidth);
                        } else if (!existingStrokeData[elementID].strokeWidth) {
                            svgDoc.getElementById(elementID).removeAttribute("stroke-width");
                        } else {
                            svgChildPart.setAttribute("stroke-width", fallbackStrokeWidth);
                        }
                    }

                    if (svgChildPart.getAttribute("stroke")) {
                        if (existingStrokeData[elementID].strokeColour) {
                            svgChildPart.setAttribute("stroke", existingStrokeData[elementID].strokeColour);
                        } else {
                            svgChildPart.setAttribute("stroke", fallbackStrokeColour);
                        }
                    }
                }
            }
        } else {
            svgPart.setAttribute("stroke-width", fallbackStrokeWidth);
            svgPart.setAttribute("stroke", fallbackStrokeColour);
        }
    }

    // Hide title box
    ePlantPlantEFPChangeTitlePosition(false);
}

/**
 * General debounce function for the ePlant Plant eFP and its tissue metadata
 * @param {Function} func Function to be debounced
 * @param {Number} wait Time to wait before executing the function
 * @returns {Function} The debounced function
 * @example <caption>Example usage of the debounce function where the function is debounced for 250ms and prevents that function from being called again during that weight time</caption>
 * debounceTissueMetadata(functionToBeDebounced, 250);
 * // returns functionToBeDebounced (after 250ms)
 */
function debounceTissueMetadata(func, wait) {
    let timeout;
    // eslint-disable-next-line func-names
    return function (...args) {
        const context = this;
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(context, args), wait);
    };
}

/**
 * Create and display the ePlant Plant eFP's hover title box
 * @param {Boolean} display Whether to display [true] or hide [false, default] the title box
 * @param {Number | String} x The x-coordinate of the element being hovered over
 * @param {Number | String} y The y-coordinate of the element being hovered over
 * @param {String} textContent The text to be displayed in the title box
 * @param {String} domID The ID of the title box DOM element
 */
function ePlantPlantEFPChangeTitlePosition(
    display = false,
    x = 0,
    y = 0,
    textContent = "",
    domID = "ePlant-hover-title-box",
) {
    /** DOM of the title box element */
    const domElm = document.getElementById(domID);

    if (domElm) {
        if (!display) {
            // Hide title box
            domElm.style.display = "none";
        } else {
            // Display title box
            domElm.style.display = "block";
            domElm.style.left = `${x}px`;
            domElm.style.top = `${y}px`;
            domElm.textContent = textContent;
        }
    }
}

/** ePlant Plant's eFP mouse event data */
const ePlantPlantEFPHandleMouseEventData = {
    /** Whether mouse events can occur [true] or not [false, default] */
    start: false,
    /** Cache last mouse position to calculate next position on drag */
    cacheMousePos: { x: null, y: null },
    /** Initial height of the SVG */
    startHeight: null,
    /** How much the SVG has been zoomed in by */
    zoomLevel: 1,
};

/**
 * Handle mouse events to drag the SVG compendium
 * @param {String} domID DOM ID of the SVG container
 * @param {String} type What type of event is happening: 'down' to initiate drag, 'move' to drag, 'up' to end drag
 * @param {Event} e Mouse event object
 * @param {Number} moveBy How much the SVG has been moved by
 */
// eslint-disable-next-line no-unused-vars
function ePlantPlantEFPHandleMouseEvent(domID, type, e, moveBy = 1.5) {
    /** SVG document */
    const svgElement = domID.firstElementChild;

    // If SVG is not loaded, then return
    if (svgElement?.viewBox?.baseVal) {
        // If the SVG is not yet been cached, then cache it
        if (!ePlantPlantEFPHandleMouseEventData.startHeight) {
            ePlantPlantEFPHandleMouseEventData.startHeight = svgElement.viewBox.baseVal.height;
        }

        // Determine if SVG should be draggable or not
        if (
            type === "down" &&
            !ePlantPlantEFPHandleMouseEventData.start &&
            window.getSelection() &&
            window.getSelection().isCollapsed
        ) {
            // Prevent highlighting of text when dragging
            e.preventDefault();

            // Cache the mouse position and begin dragging
            ePlantPlantEFPHandleMouseEventData.start = true;
            ePlantPlantEFPHandleMouseEventData.cacheMousePos = { x: e.clientX, y: e.clientY };
        }

        // If the SVG is being dragged, then drag it
        if (type === "move" && ePlantPlantEFPHandleMouseEventData.start) {
            // Prevent highlighting of text when dragging
            e.preventDefault();

            /** How much the SVG will be dragged */
            const moveByValue = svgElement.viewBox.baseVal.height
                ? window.innerHeight / svgElement.viewBox.baseVal.height / moveBy
                : moveBy;

            // Calculate the new position of the SVG
            /** New X position of SVG */
            const xDiff = -(e.clientX - ePlantPlantEFPHandleMouseEventData.cacheMousePos.x) / moveByValue;
            /** New Y position of SVG */
            const yDiff = -(e.clientY - ePlantPlantEFPHandleMouseEventData.cacheMousePos.y) / moveByValue;

            // Find boundaries of the SVG so it does not leave viewpoint
            /** Default boundaries for SVG viewpoint */
            const defaultScaleBoundaries = 0.95;

            /** Boundaries to scale the SVG's viewpoint on the X axis */
            let scaleBoundariesX = svgElement.height.baseVal.value
                ? (defaultScaleBoundaries * 100 - svgElement.height.baseVal.value / svgElement.viewBox.baseVal.height) /
                    100
                : (defaultScaleBoundaries * 100 - window.innerHeight / svgElement.viewBox.baseVal.height) / 100;
            // Should be between 0 and 1
            if (scaleBoundariesX <= 0 || scaleBoundariesX >= 1) {
                scaleBoundariesX = defaultScaleBoundaries;
            }

            /** Boundaries to scale the SVG's viewpoint on the Y axis */
            let scaleBoundariesY = svgElement.width.baseVal.value
                ? (defaultScaleBoundaries * 100 - svgElement.width.baseVal.value / svgElement.viewBox.baseVal.width) /
                    100
                : (defaultScaleBoundaries * 100 - window.innerWidth / svgElement.viewBox.baseVal.height) / 100;
            // Should be between 0 and 1
            if (scaleBoundariesY <= 0 || scaleBoundariesY >= 1) {
                scaleBoundariesY = defaultScaleBoundaries;
            }

            /** Current zoom level on the SVG compendium */
            const zoomLevel =
                1 / ePlantPlantEFPHandleMouseEventData.zoomLevel === 1
                    ? defaultScaleBoundaries
                    : 1 / ePlantPlantEFPHandleMouseEventData.zoomLevel;

            /** Boundaries to scale the SVG's viewpoint on the X axis */
            const xBoundaries = svgElement.viewBox.baseVal.width * scaleBoundariesX * zoomLevel;
            /** Boundaries for the X axis on the right side of the SVG compendium's viewpoint */
            const xRightBoundaries = svgElement.viewBox.baseVal.width * scaleBoundariesX;

            /** Boundaries to scale the SVG's viewpoint on the Y axis */
            const yBoundaries = svgElement.viewBox.baseVal.height * scaleBoundariesY;
            /** Upper boundaries to scale the SVG's viewpoint on the Y axis */
            const yUpperBoundaries = yBoundaries * zoomLevel;

            // Cache mouse position
            ePlantPlantEFPHandleMouseEventData.cacheMousePos = { x: e.clientX, y: e.clientY };

            // If SVG's Y position within viewpoint, then move it
            if (
                svgElement.viewBox.baseVal.y + yDiff <= yUpperBoundaries &&
                svgElement.viewBox.baseVal.y + yDiff >= -yBoundaries
            ) {
                svgElement.viewBox.baseVal.y += yDiff;
            } else {
                // If SVG's Y position is outside viewpoint, then move it to the top or bottom
                svgElement.viewBox.baseVal.y =
                    svgElement.viewBox.baseVal.y + yDiff > 0 ? yUpperBoundaries : -yBoundaries;
            }

            // If SVG's X position within viewpoint, then move it
            if (
                svgElement.viewBox.baseVal.x + xDiff <= xBoundaries &&
                svgElement.viewBox.baseVal.x + xDiff >= -xRightBoundaries
            ) {
                svgElement.viewBox.baseVal.x += xDiff;
            } else {
                // If SVG's X position is outside viewpoint, then move it to the left or right
                svgElement.viewBox.baseVal.x =
                    svgElement.viewBox.baseVal.x + xDiff > 0 ? xBoundaries : -xRightBoundaries;
            }
        }
    }

    // End dragging
    if (type === "up") {
        ePlantPlantEFPHandleMouseEventData.start = false;
    }
}

/**
 * Handle zooming of SVG
 * @param {String} domID DOM ID of the SVG container
 * @param {Event} e Mouse event object
 * @param {Number} changeBy How much the SVG has been moved by
 */
// eslint-disable-next-line no-unused-vars
function ePlantPlantEFPHandleMouseWheel(domID, e, changeBy = 3) {
    /** SVG document */
    const svgElement = domID.firstElementChild;
    /** SVG viewpoint */
    const baseValues = svgElement.viewBox.baseVal;

    /** If should zoom in [true] or out [false] */
    const up = e.deltaY > 0;

    /** How much the zoom will zoom in by */
    const changeByValue = ePlantPlantEFPHandleMouseEventData.startHeight
        ? window.innerHeight / ePlantPlantEFPHandleMouseEventData.startHeight / changeBy
        : changeBy;

    if (
        window.getSelection() &&
        window.getSelection().isCollapsed &&
        baseValues &&
        e.deltaY &&
        ePlantPlantEFPHandleMouseEventData.start
    ) {
        // Prevent scrolling window
        e.preventDefault();

        if (up) {
            svgElement.viewBox.baseVal.width = baseValues.width * changeByValue;
            svgElement.viewBox.baseVal.height = baseValues.height * changeByValue;
            ePlantPlantEFPHandleMouseEventData.zoomLevel *= changeByValue;
        } else {
            svgElement.viewBox.baseVal.width = baseValues.width / changeByValue;
            svgElement.viewBox.baseVal.height = baseValues.height / changeByValue;
            ePlantPlantEFPHandleMouseEventData.zoomLevel /= changeByValue;
        }
    }
}

/**
 * Create and retrieve expression data in an SVG format
 */
class CreateSVGExpressionData {
    constructor() {
        // callPlantEFP
        this.eFPObjects = {};

        // loadSampleData
        this.sampleData = {};
        this.sampleOptions = [];
        this.sampleReadableName = {};

        // Top expression data
        this.topExpressionValues = {};
        this.expressionValues = {};
        this.topExpressionOptions = ["Microarray", "RNA-seq"];

        // Local storage grabbed
        this.localStorageTop = false;
        this.localStorageSample = {};

        // Local for this class
        this.desiredDOMid = "";
        /** Markup for the visualization container */
        // eslint-disable-next-line no-unused-expressions
        this.appendSVG;

        // createSVGValues
        this.clickList = [];
        this.svgValues = {};
        this.svgMax = 0;
        this.svgMin = 0;
        this.svgMaxAverage = 0;
        this.svgMaxAverageSample = "";
        this.svgMinAverage = 0;
        this.svgMinAverageSample = "";
        // Store object name:
        this.svgObjectName = "";

        /** SVG DOM container's height styling */
        this.svgContainerHeight = "95vh";
    }

    /**
     * Verify that the locus being called is valid
     * IMPORTANT: The current script only works for Arabidopsis thaliana
     * TODO: Add support for other languages. Fill list of loci patterns can be found within GAIA's tools (accessible only to BAR developer at the moment)
     * @param {String} locus The AGI ID (example: AT3G24650 or AT3G24650.1)
     * @returns {Boolean} If locus is valid [true] or not [false, default]
     */
    // eslint-disable-next-line class-methods-use-this
    verifyLoci(locus) {
        // Check if locus is a string
        if (typeof locus === "string") {
            /** Arabidopsis thaliana locus pattern */
            const arabidopsisThalianaPattern = `^[A][T][MC0-9][G][0-9]{5}[.][0-9]{1,2}$|^[A][T][MC0-9][G][0-9]{5}$`;

            /** Reg Exp for the locus pattern */
            const regexPattern = new RegExp(arabidopsisThalianaPattern, "i");

            // If match, then return true, else return false
            return Boolean(locus.trim().match(regexPattern));
        }
        return false;
    }

    /**
     * Create and generate an SVG based on the desired tissue expression locus
     * @param {String} locus The AGI ID (example: AT3G24650)
     * @param {String} desiredDOMid The desired DOM location or if kept empty, would not replace any DOM elements and just create the related HTML DOM elements within appendSVG
     * @param {String} svgName Name of the SVG file without the .svg at the end. Default is set to "default", when left this value, the highest expression value (if any) is chosen and if not, then Abiotic Stress is.
     * @param {Boolean} includeDropdownAll true = include a html dropdown/select of all available SVGs/samples, false = don't
     * @param {String | Number} containerHeight The height of the SVG container, default is 95vh
     */
    generateSVG(
        locus = "AT3G24650",
        desiredDOMid = undefined,
        svgName = "default",
        includeDropdownAll = true,
        containerHeight = undefined,
    ) {
        if (this.verifyLoci(locus.trim())) {
            // Reset variables:
            this.svgValues = {};
            this.svgMax = undefined;
            this.svgMin = undefined;
            this.svgMaxAverage = undefined;
            this.svgMaxAverageSample = undefined;
            this.svgMinAverage = undefined;
            this.svgMinAverageSample = undefined;
            this.includeDropdownAll = includeDropdownAll;
            if (this.clickList.includes(svgName) === false) {
                this.clickList.push(svgName);
            }
            if (containerHeight && typeof containerHeight === "string") {
                this.svgContainerHeight = containerHeight.toString();
            } else if (containerHeight && typeof containerHeight === "number") {
                this.svgContainerHeight = `${containerHeight.toString()}px`;
            }

            // Initiate scripts
            this.desiredDOMid = desiredDOMid;
            this.#retrieveTopExpressionValues(svgName, locus.trim().toUpperCase());
        } else {
            console.error(`Invalid locus: ${locus.trim()}`);
        }
    }

    /**
     * Retrieve information about the top expression values for a specific locus
     * @param {String} svgName Name of the SVG file without the .svg at the end
     * @param {String} locus The AGI ID (example: AT3G24650)
     */
    async #retrieveTopExpressionValues(svgName, locus = "AT3G24650") {
        let completedFetches = 0;

        let localStorageTopExpressionValues = localStorage.getItem("bar_eplant-top-expression-values");
        let fetchData = true;
        if (!this.localStorageTop) {
            if (localStorageTopExpressionValues) {
                localStorageTopExpressionValues = JSON.parse(localStorageTopExpressionValues);

                // Check if week passed expiration
                if (
                    localStorageTopExpressionValues.expiry &&
                    new Date().getTime() - localStorageTopExpressionValues.expiry <= 7 * 24 * 60 * 60 * 1000
                ) {
                    fetchData = false;
                } else if (localStorageTopExpressionValues[locus]) {
                    fetchData = false;
                }

                this.topExpressionValues = {
                    ...this.topExpressionValues,
                    ...localStorageTopExpressionValues,
                };
                this.localStorageTop = true;
            }
        }

        if (this.topExpressionValues[locus]) {
            fetchData = false;
        }

        // If never been called before
        if (fetchData) {
            for (const topMethod of this.topExpressionOptions) {
                const url = `https://bar.utoronto.ca/expression_max_api/max_average?method=${topMethod}`;
                const sendHeaders = "application/json";
                let postSend = {
                    loci: [locus.toUpperCase()],
                    method: topMethod,
                };
                postSend = JSON.stringify(postSend);

                const methods = { mode: "cors" };
                methods.method = "POST";
                if (sendHeaders) {
                    methods.headers = {};
                    methods.headers["Content-type"] = sendHeaders;
                }
                methods.body = postSend;

                // eslint-disable-next-line no-await-in-loop
                await fetch(url, methods)
                    // eslint-disable-next-line no-loop-func
                    .then(async (response) => {
                        if (response.status === 200) {
                            await response.text().then(async (data) => {
                                let responseData;
                                if (data.length > 0) {
                                    responseData = JSON.parse(data);
                                } else {
                                    responseData = {};
                                }

                                let topMethodUsed;
                                const urlQuery = url.split("=");
                                if (urlQuery.length > 1) {
                                    topMethodUsed = urlQuery[1];
                                }

                                if (topMethodUsed && responseData && responseData.wasSuccessful === true) {
                                    if (responseData.maxAverage) {
                                        const tempTopExpressionData = {};
                                        tempTopExpressionData[topMethodUsed] = {};

                                        tempTopExpressionData[topMethodUsed].maxAverage =
                                            responseData.maxAverage[locus.toUpperCase()];

                                        if (responseData.standardDeviation) {
                                            tempTopExpressionData[topMethodUsed].standardDeviation =
                                                responseData.standardDeviation[locus.toUpperCase()];
                                        }
                                        if (responseData.sample) {
                                            tempTopExpressionData[topMethodUsed].sample =
                                                responseData.sample[locus.toUpperCase()];
                                        }
                                        if (responseData.compendium) {
                                            tempTopExpressionData[topMethodUsed].compendium =
                                                responseData.compendium[locus.toUpperCase()];
                                        }

                                        if (!this.topExpressionValues) {
                                            this.topExpressionValues = {};
                                        }
                                        this.topExpressionValues[locus] = {
                                            ...this.topExpressionValues[locus],
                                            ...tempTopExpressionData,
                                        };
                                    }
                                }

                                completedFetches += 1;
                                if (completedFetches === this.topExpressionOptions.length) {
                                    // Update local storage:
                                    // Add to local storage as well:
                                    if (!localStorageTopExpressionValues || fetchData) {
                                        localStorageTopExpressionValues = {};

                                        localStorageTopExpressionValues[locus] = {
                                            ...this.topExpressionValues[locus],
                                        };

                                        localStorageTopExpressionValues.expiry = new Date().getTime();
                                    } else {
                                        localStorageTopExpressionValues[locus] = {
                                            ...this.topExpressionValues[locus],
                                        };
                                    }
                                    // Ensure that local storage has no more than 10 entries
                                    if (
                                        localStorageTopExpressionValues &&
                                        Object.keys(localStorageTopExpressionValues).length > 10
                                    ) {
                                        // Remove the first entry if not the one we just added and not 'expiry'
                                        for (const key in localStorageTopExpressionValues) {
                                            if (key !== locus && key !== "expiry") {
                                                delete localStorageTopExpressionValues[key];
                                                break;
                                            }
                                        }
                                    }

                                    if (localStorageTopExpressionValues) {
                                        localStorage.setItem(
                                            "bar_eplant-top-expression-values",
                                            JSON.stringify(localStorageTopExpressionValues),
                                        );
                                        this.localStorageTop = true;
                                    }

                                    await this.#loadSampleData(svgName, locus);
                                }
                            });
                        } else if (response.status !== 200) {
                            completedFetches += 1;
                            if (completedFetches === this.topExpressionOptions.length) {
                                await this.#loadSampleData(svgName, locus);
                            }

                            console.error(
                                `fetch error - Status Code: ${response.status}, fetch-url: ${response.url}, document-url: ${window.location.href}`,
                            );
                        }
                    })
                    // eslint-disable-next-line no-loop-func
                    .catch(async (err) => {
                        completedFetches += 1;
                        if (completedFetches === this.topExpressionOptions.length) {
                            await this.#loadSampleData(svgName, locus);
                        }

                        console.error(err);
                    });
            }
        } else if (Object.keys(this.topExpressionValues[locus]).length > 0) {
            await this.#loadSampleData(svgName, locus);
        } else if (Object.keys(localStorageTopExpressionValues[locus]).length > 0) {
            if (!this.topExpressionValues) {
                this.topExpressionValues = {};
            }

            this.topExpressionValues[locus] = localStorageTopExpressionValues[locus];
            await this.#loadSampleData(svgName, locus);
        }
    }

    /**
     * Calls and stores the sample Data for the SVG, SVG's subunits, datasource and it's name-values
     * @param {String} svgName Name of the SVG file without the .svg at the end
     * @param {String} locus The AGI ID (example: AT3G24650)
     */
    async #loadSampleData(svgName, locus) {
        if (Object.keys(this.sampleData).length === 0) {
            /** Whether to fetch the sample data container from GitHub (true, default) or not */
            let fetchFromGitHub = true;

            /** Browser's local storage for the sample data, if exists */
            let localStoredSampleData = localStorage?.getItem("bar_eplant-sample-data-storage");
            // If the local storage exists, see if it's expired (1 week) to fetch from GitHub or use the local storage data
            if (localStoredSampleData) {
                // Convert to JSON
                localStoredSampleData = JSON.parse(localStoredSampleData);

                // If a week has passed since the last time the data was stored
                if (
                    localStoredSampleData.expiry &&
                    localStoredSampleData.data &&
                    new Date().getTime() - localStoredSampleData.expiry <= 7 * 24 * 60 * 60 * 1000
                ) {
                    // Has not expire, use this data
                    fetchFromGitHub = false;

                    this.sampleData = localStoredSampleData.data;
                }
            }

            if (fetchFromGitHub) {
                /** GitHub's URL for the sample data container */
                const url =
                    "https://raw.githubusercontent.com/BioAnalyticResource/ePlant_Plant_eFP/master/data/SampleData.min.json";

                /** Fetch methods */
                const methods = { mode: "cors" };

                await fetch(url, methods)
                    .then(async (response) => {
                        if (response.status === 200) {
                            await response.text().then(async (data) => {
                                /** Response data */
                                const res = data.length > 0 ? JSON.parse(data) : {};

                                // Store response
                                this.sampleData = res;
                                // Store into local storage
                                const sampleDataStorage = {
                                    data: res,
                                    expiry: new Date().getTime(),
                                };
                                localStorage.setItem(
                                    "bar_eplant-sample-data-storage",
                                    JSON.stringify(sampleDataStorage),
                                );
                            });
                        } else if (response.status !== 200) {
                            console.error(
                                `fetch error - Status Code: ${response.status}, fetch-url: ${response.url}, document-url: ${window.location.href}`,
                            );
                        }
                    })
                    .catch(async (err) => {
                        console.error(err);
                    });
            }

            // Setup and retrieve information about the target SVG and locus
            await this.#retrieveSampleData(svgName, locus);
        } else if (Object.keys(this.sampleData).length > 0) {
            await this.#retrieveSampleData(svgName, locus);
        }
    }

    /**
     * Retrieves the sample information relating to an SVG for a specific set of data
     * @param {String} svgName Name of the SVG file without the .svg at the end
     * @param {String} locus The AGI ID (example: AT3G24650)
     */
    async #retrieveSampleData(svgName, locus) {
        // Check if svgName contains .svg
        if (svgName.substring(-4) === ".svg") {
            svgName = svgName.substring(0, svgName.length - 4);
        }

        if (this.sampleOptions.length === 0) {
            for (const [key, value] of Object.entries(this.sampleData)) {
                this.sampleOptions.push(key);
                this.sampleReadableName[value.name] = key;
            }
        }

        // Create variables that will be used in #retrieveSampleData
        const sampleDataKeys = Object.keys(this.sampleData); // All possible SVGs
        let sampleDB = ""; // The sample's datasource
        let sampleIDList = []; // List of all of the sample's IDs
        let sampleSubunits = []; // List of SVG's subunits

        await this.#processLocalStorageEFPObjectData();

        // Check if valid SVG
        if (!sampleDataKeys.includes(svgName) && this.topExpressionValues[locus]) {
            // Determine max expression value to default too
            let maxExpressionValue = 0;
            let maxExpressionCompendium;
            for (const [, value] of Object.entries(this.topExpressionValues[locus])) {
                if (value.compendium && value.compendium[1] && sampleDataKeys.includes(value.compendium[1])) {
                    if (value.maxAverage && value.maxAverage[1] && value.maxAverage[1] > maxExpressionValue) {
                        maxExpressionValue = value.maxAverage[1];
                        maxExpressionCompendium = value.compendium[1];
                    }
                }
            }

            if (maxExpressionCompendium) {
                svgName = maxExpressionCompendium;
            } else {
                svgName = "AbioticStress";
            }
        }

        // If still default, load in Abiotic Stress
        if (svgName === "default") {
            svgName = "AbioticStress";
        }

        // Create variables for parsing
        const sampleInfo = this.sampleData[svgName];
        let sampleOptions;
        if (sampleInfo && sampleInfo.sample) {
            sampleOptions = sampleInfo.sample;
        }
        if (sampleInfo && sampleInfo.db) {
            sampleDB = sampleInfo.db;
        }

        // If a database is available for this SVG, then find sample ID information
        if (sampleDB !== undefined) {
            sampleSubunits = Object.keys(sampleInfo.sample);
            sampleIDList = [];
            for (const sample of sampleSubunits) {
                sampleIDList = sampleIDList.concat(sampleOptions[sample]);
            }
        }

        // Call plantefp.cgi webservice to retrieve information about the target tissue expression data
        if (!this.eFPObjects[svgName] || !this.eFPObjects[svgName].locusCalled.includes(locus)) {
            await this.#callPlantEFP(sampleDB, locus, sampleIDList, svgName, sampleOptions);
        } else if (this.eFPObjects[svgName]) {
            await this.#addSVGtoDOM(svgName, locus, this.includeDropdownAll);
        }
    }

    async #processLocalStorageEFPObjectData() {
        if (Object.keys(this.localStorageSample).length === 0) {
            // Grab localStorage's sample data:
            let localStorageSampleValues = localStorage.getItem("bar_eplant-efp-data");
            if (localStorageSampleValues) {
                localStorageSampleValues = JSON.parse(localStorageSampleValues);
                this.localStorageSample = localStorageSampleValues;
                await this.#checkLocalStorageEFPObjectSize();

                // Check if week passed expiration
                if (
                    !localStorageSampleValues.expiry ||
                    !(new Date().getTime() - localStorageSampleValues.expiry <= 7 * 24 * 60 * 60 * 1000)
                ) {
                    // If this.eFPObjects is empty, give it the localStorageSampleValues data
                    if (Object.keys(this.eFPObjects).length === 0) {
                        this.eFPObjects = localStorageSampleValues;
                    } else {
                        // Go through this.eFPObjects and localStorageSampleValues and add in any missing data
                        // Data is in format of this.eFPObjects[compendium] and in [compendium], see if there is a [locus].
                        // See which loci are missing and add them in (if any), and if so, go through the [sample] and add in any missing data
                        for (const [compendium, compendiumData] of Object.entries(localStorageSampleValues)) {
                            if (compendium !== "expiry") {
                                if (this.eFPObjects[compendium]) {
                                    // Check if locus is missing
                                    const missingLoci = [];
                                    // Go through the [locusCalled] array and see if any are missing
                                    for (const locus of compendiumData.locusCalled) {
                                        if (!this.eFPObjects[compendium].locusCalled.includes(locus)) {
                                            missingLoci.push(locus);
                                        }
                                    }

                                    // If there are missing loci, add them in
                                    if (missingLoci.length > 0) {
                                        // Go through the sample data and add in any missing data
                                        for (const [sample, sampleData] of Object.entries(compendiumData.sample)) {
                                            if (this.eFPObjects[compendium].sample[sample]) {
                                                // Go through the [locus] and see if any are missing
                                                for (const locus of missingLoci) {
                                                    if (!this.eFPObjects[compendium].sample[sample][locus]) {
                                                        this.eFPObjects[compendium].sample[sample][locus] =
                                                            sampleData[locus];
                                                    }
                                                }
                                            } else {
                                                this.eFPObjects[compendium].sample[sample] = sampleData;
                                            }
                                        }
                                    }
                                } else {
                                    this.eFPObjects[compendium] = compendiumData;
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    async #checkLocalStorageEFPObjectSize(svgKeep = undefined) {
        // Ensure that the localStorage is not too large (no more than 1MB)
        // If it is, then remove the the first SVG in the eFPObjects object if not the one that is currently being kept (svgKeep) or 'expiry'

        // Check size of this.localStorageSample
        // Convert to string
        const localStorageSampleString = JSON.stringify(this.localStorageSample);
        // Get size of string
        const localStorageSampleSize = localStorageSampleString.length * 2;
        // If the localStorageSampleSize is too large, remove the first SVG in the eFPObjects object if not the one that is currently being kept (svgKeep) or 'expiry'
        if (localStorageSampleSize > 1000000) {
            // Go through the eFPObjects object and remove the first SVG if not the one that is currently being kept (svgKeep) or 'expiry'
            for (const [svgName, _svgData] of Object.entries(this.eFPObjects)) {
                if (svgName !== svgKeep && svgName !== "expiry") {
                    delete this.eFPObjects[svgName];
                    break;
                }
            }
        }
    }

    /**
     * Calls the plantefp.cgi webservice to retrieve expression data from the BAR
     * @param {String} datasource Which database the information is contained in
     * @param {String} locus The AGI ID (example: AT3G24650)
     * @param {Array} samples List of sample ID's which the exact expression data is related to
     * @param {String} svg Which SVG is being called
     * @param {Array} sampleSubunits List of the SVG's subunits
     */
    async #callPlantEFP(datasource, locus, samples, svg, sampleSubunits) {
        // Create URL
        let url = "https://bar.utoronto.ca/~asullivan/webservices/plantefp.cgi?";
        url += `datasource=${datasource}&`;
        url += `id=${locus}&`;
        url += "samples=[";
        for (let i = 0; i < samples.length; i += 1) {
            let sampleName = samples[i].trim();
            sampleName = sampleName.replace(/\+/g, "%2B");
            sampleName = sampleName.replace(/ /g, "%20");

            url += `"${sampleName}"`;
            if (i !== samples.length - 1) {
                url += ",";
            }
        }
        url += "]";

        const methods = { mode: "cors" };

        let alreadyRetrievedData = false;
        if (this.eFPObjects?.[svg]?.[locus]?.includes("locus")) {
            alreadyRetrievedData = true;
        }

        if (sampleSubunits && !alreadyRetrievedData) {
            await fetch(url, methods)
                .then(async (response) => {
                    if (response.status === 200) {
                        await response.text().then(async (data) => {
                            let responseData;
                            if (data.length > 0) {
                                responseData = JSON.parse(data);
                            } else {
                                responseData = {};
                            }

                            const subunitsList = Object.keys(sampleSubunits);

                            if (this.eFPObjects === undefined) {
                                this.eFPObjects = {};
                            }

                            // Create SVG in dictionary
                            if (this.eFPObjects[svg] === undefined) {
                                this.eFPObjects[svg] = {};
                            }

                            // Create samples in dictionary
                            if (this.eFPObjects[svg].sample === undefined) {
                                this.eFPObjects[svg].sample = {};
                            }

                            // Create samples in dictionary
                            if (this.eFPObjects[svg].sample === undefined) {
                                this.eFPObjects[svg].sample = {};
                            }

                            // Add values
                            for (const resData of responseData) {
                                // Create key and value variables
                                const responseName = resData.name.trim();
                                const responseValue = resData.value;
                                let subunitName = "";

                                // Create subunits element in dictionary
                                let tempName = responseName;
                                tempName = responseName.replace(/%2B/g, "+");
                                tempName = tempName.replace(/%20/g, " ");
                                tempName = tempName.trim();

                                for (const subunit of subunitsList) {
                                    if (sampleSubunits[subunit].includes(tempName)) {
                                        subunitName = subunit;

                                        // Create subunit
                                        if (this.eFPObjects[svg].sample[subunitName] === undefined) {
                                            this.eFPObjects[svg].sample[subunitName] = {};
                                        }

                                        // Create responseName
                                        if (this.eFPObjects[svg].sample[subunitName][tempName] === undefined) {
                                            this.eFPObjects[svg].sample[subunitName][tempName] = {};
                                        }

                                        // Add to dictionary
                                        this.eFPObjects[svg].sample[subunitName][tempName][locus] = responseValue;

                                        // Add to list of called locus data
                                        if (!this.eFPObjects[svg].locusCalled) {
                                            this.eFPObjects[svg].locusCalled = [];
                                        }
                                        if (!this.eFPObjects[svg].locusCalled.includes(locus)) {
                                            this.eFPObjects[svg].locusCalled.push(locus);
                                        }
                                    }
                                }
                            }

                            // Add db
                            this.eFPObjects[svg].db = datasource;

                            // Update local storage
                            if (!this.localStorageSample || Object.keys(this.localStorageSample)?.length === 0) {
                                this.localStorageSample = this.eFPObjects;
                                this.localStorageSample.expiry = new Date().getTime();
                            } else {
                                this.localStorageSample[svg] = this.eFPObjects[svg];
                            }
                            await this.#checkLocalStorageEFPObjectSize(svg);
                            localStorage.setItem("bar_eplant-efp-data", JSON.stringify(this.localStorageSample));

                            await this.#addSVGtoDOM(svg, locus, this.includeDropdownAll);
                        });
                    } else if (response.status !== 200) {
                        await this.#addSVGtoDOM(svg, locus, this.includeDropdownAll);

                        console.error(
                            `fetch error - Status Code: ${response.status}, fetch-url: ${response.url}, document-url: ${window.location.href}`,
                        );
                    }
                })
                .catch(async (err) => {
                    await this.#addSVGtoDOM(svg, locus, this.includeDropdownAll);

                    console.error(err);
                });
        } else {
            await this.#addSVGtoDOM(svg, locus, this.includeDropdownAll);

            console.error(`sampleSubunits is ${sampleSubunits}`);
        }
    }

    /**
     * Add the SVG to the designated DOM
     * @param {String} svgName Name of the SVG file without the .svg at the end
     * @param {String} locus The AGI ID (example: AT3G24650)
     */
    async #addSVGtoDOM(svgName, locus, includeDropdownAll = false) {
        let svgUse = "Klepikova";
        let localAppendSVG = new DOMParser().parseFromString('<div class="expressionContainer"></div>', "text/html");
        localAppendSVG = localAppendSVG.querySelector(".expressionContainer");
        if (svgName !== "") {
            svgUse = svgName;
        }

        // Add dropdown list of all samples to document:
        if (includeDropdownAll && this.sampleOptions) {
            let selectedIndexPos = 0;
            let preSelectedIndex = 0;
            let options = "";

            if (this.topExpressionValues[locus] && Object.keys(this.topExpressionValues[locus]).length > 0) {
                // Hidden option
                options += `<option
                        value="hiddenOption"
                        id="hiddenExpressionOption"
                        disabled="true"
                    >
                        Compendiums with maximum average expression:
                    </option>`;
                preSelectedIndex += 1;

                const topList = Object.keys(this.topExpressionValues[locus]);

                for (const top of topList) {
                    if (this.topExpressionValues[locus][top]) {
                        const expressionData = this.topExpressionValues[locus][top];
                        const compendiumOptions = expressionData.compendium;

                        for (let c = 0; c < Object.keys(compendiumOptions).length; c += 1) {
                            const cUse = c + 1;

                            if (expressionData.compendium[cUse]) {
                                const expressionCompendium = expressionData.compendium[cUse];
                                const expressionSample = expressionData.sample[cUse];

                                if (
                                    expressionSample &&
                                    this.sampleData[expressionCompendium] &&
                                    this.sampleData[expressionCompendium] &&
                                    this.sampleData[expressionCompendium].description
                                ) {
                                    const readableSampleName =
                                        this.sampleData[expressionCompendium].description[expressionSample];
                                    const expressionAverageLevel = expressionData.maxAverage[cUse];
                                    const compendiumName = this.sampleData[expressionCompendium].name;

                                    options += `<option
                                            value="${expressionCompendium}"
                                        >
                                            ${compendiumName}: ${readableSampleName} at ${expressionAverageLevel} (${top})
                                        </option>`;

                                    preSelectedIndex += 1;

                                    break;
                                }
                            }
                        }
                    }
                }
            }

            options += `<option
                    value="hiddenOption"
                    id="allCompendiumOptions"
                    disabled="true"
                >
                    All compendiums:
                </option>`;

            preSelectedIndex += 1;

            const sampleOptions = Object.keys(this.sampleReadableName);
            sampleOptions.sort();

            for (const i in sampleOptions) {
                if (sampleOptions[i]) {
                    options += `<option
                        value="${this.sampleReadableName[sampleOptions[i]]}"
                    >
                        ${sampleOptions[i]}
                    </option>`;

                    if (this.sampleReadableName[sampleOptions[i]] === svgName) {
                        selectedIndexPos = parseInt(preSelectedIndex, 10) + parseInt(i, 10);
                    }
                }
            }

            const dropdownList = new DOMParser().parseFromString(
                `<div class="selectSVGContainer">
                    <span>Select SVG to display:</span>
                    <select
                        onchange="window.createSVGExpressionData.generateSVG('${locus}', '${this.desiredDOMid}', this?.value?.toString()?.length > 0 ? this.value.toString() : 'default', ${includeDropdownAll})"
                        id="sampleOptions"
                        value="${svgName}"
      style="width: 100%; max-width: 40em;"
                        class="selectCompendiumOptions"
                        aria-label="Select SVG to display"
                    >
                        ${options}
                    </select>
                </div>`,
                "text/html",
            ).body.childNodes[0];
            dropdownList.getElementsByTagName("select")[0].selectedIndex = selectedIndexPos;
            localAppendSVG.appendChild(dropdownList);
        }

        const titleBoxDOM = new DOMParser().parseFromString(
            `<div
                id="ePlant-hover-title-box"
                style="
                    position: fixed;
                    z-index: 100;
                    color: #fff;
                    background-color: #000;
                    border: 1px solid #000;
                    border-radius: 3px;
                    padding: 5px;
                    opacity: 0.85;
                    white-space: pre-line;
                    display: none;
                "
            >Test</div>`,
            "text/html",
        ).body.childNodes[0];
        localAppendSVG.appendChild(titleBoxDOM);

        // Create call for SVG file
        const urlSVG = `https://bar.utoronto.ca/~asullivan/ePlant_Plant_eFP/compendiums/${svgUse}.svg`;
        const methods = { mode: "cors" };

        await fetch(urlSVG, methods)
            .then(async (response) => {
                if (response.status === 200) {
                    await response.text().then(async (data) => {
                        const svgData = new DOMParser().parseFromString(data, "text/html").body.childNodes[0];

                        /** Adjust styling of SVG */
                        if (svgData.id) {
                            this.svgObjectName = svgData.id;

                            svgData.style = "width: 100% !important; height: 100% !important;";
                        }

                        const svgContainer = new DOMParser().parseFromString(
                            `<div id="${svgUse}_object" style="height:${this.svgContainerHeight};"
                                onMouseDown="ePlantPlantEFPHandleMouseEvent(${svgUse}_object, 'down', event)"
                                onMouseMove="ePlantPlantEFPHandleMouseEvent(${svgUse}_object, 'move', event)"
                                onMouseUp="ePlantPlantEFPHandleMouseEvent(${svgUse}_object, 'up', event)"
                                onMouseLeave="ePlantPlantEFPHandleMouseEvent(${svgUse}_object, 'up', event)"
                                onWheel="ePlantPlantEFPHandleMouseWheel(${svgUse}_object, event)"
                            ></div>`,
                            "text/html",
                        ).body.childNodes[0];
                        svgContainer.appendChild(svgData);

                        localAppendSVG.appendChild(svgContainer);

                        this.appendSVG = localAppendSVG;

                        await this.#createLocusMatch(svgUse, locus);
                    });
                } else if (response.status !== 200) {
                    console.error(
                        `fetch error - Status Code: ${response.status}, fetch-url: ${response.url}, document-url: ${window.location.href}`,
                    );
                }
            })
            .catch(async (err) => {
                console.error(err);
            });

        if (this.desiredDOMid && this.desiredDOMid.length > 0) {
            // Add SVG to DOM
            /** DOM Region being modified */
            const targetDOMRegion = document.getElementById(this.desiredDOMid);

            if (targetDOMRegion) {
                if (targetDOMRegion.childNodes && targetDOMRegion.childNodes.length > 0) {
                    const targetDOMChildren = targetDOMRegion.childNodes;
                    for (const i in targetDOMChildren) {
                        if (
                            targetDOMChildren[i].className &&
                            targetDOMChildren[i].className.includes("expressionContainer")
                        ) {
                            targetDOMRegion.removeChild(targetDOMChildren[i]);
                        }
                    }
                }

                targetDOMRegion.appendChild(this.appendSVG);
            }
        }
    }

    /**
     * Check and verify locus name
     * @param {String} whichSVG Name of the SVG file without the .svg at the end
     * @param {String} locus The AGI ID (example: AT3G24650)
     */
    async #createLocusMatch(whichSVG, locus) {
        const locusPoint = locus;

        let locusValue = "";
        for (let i = 0; i < locusPoint.length; i += 1) {
            if (i === 1 || i === 3) {
                locusValue += locusPoint[i].toLowerCase();
            } else {
                // eslint-disable-next-line no-unused-vars
                locusValue += locusPoint[i];
            }
        }

        await this.#createSVGValues(whichSVG, locus);
    }

    /**
     * Retrieves and stores raw values based on the searched SVG
     * @param {String} whichSVG Name of the SVG file without the .svg at the end
     * @param {String} locus The AGI ID (example: AT3G24650)
     */
    async #createSVGValues(whichSVG, locus) {
        // Retrieve tissue expression information
        const dataObject = this.eFPObjects;
        const svgDataObject = dataObject[whichSVG].sample;

        // Find tissue expression's sample IDs
        const svgSubunits = Object.keys(svgDataObject);

        // Find respective values
        for (const svg of svgSubunits) {
            const sampleValues = Object.keys(svgDataObject[svg]);
            // Create SVG subunit in dictionary
            if (this.svgValues[svg] === undefined) {
                this.svgValues[svg] = {};
            }
            // Add raw values
            for (const sample of sampleValues) {
                // Create raw values in dictionary
                if (this.svgValues[svg].rawValues === undefined) {
                    this.svgValues[svg].rawValues = [];
                }
                this.svgValues[svg].rawValues.push(svgDataObject[svg][sample][locus]);
            }
        }
        await this.#findExpressionValues(whichSVG, svgSubunits);
    }

    /**
     * Find the maximum, minimum and average values
     * @param {String} whichSVG Name of the SVG file without the .svg at the end
     * @param {Array} svgSubunits A list containing all desired SVG subunits to be interacted with
     */
    async #findExpressionValues(whichSVG, svgSubunits) {
        // Reset variables
        this.svgMax = undefined;
        this.svgMin = undefined;

        // Iterate over each SVG subunit for their respective values
        for (const svg of svgSubunits) {
            const values = this.svgValues[svg].rawValues.sort();
            const numValues = [];
            for (const value of values) {
                if (Number.isNaN(value) === false) {
                    numValues.push(parseFloat(value));
                }
            }

            // Find averages
            let sumValues = 0;
            for (const num of numValues) {
                sumValues += num;
            }
            const averageValues = sumValues / numValues.length;

            // Compare max values
            const maxValue = numValues[numValues.length - 1];
            const minValue = numValues[1];
            if (!this.svgMax || maxValue > this.svgMax) {
                this.svgMax = maxValue;
            }

            if (!this.svgMin || minValue < this.svgMin) {
                this.svgMin = minValue;
            }

            // Now for averages:
            if (!this.svgMaxAverage || averageValues > this.svgMaxAverage) {
                this.svgMaxAverage = averageValues;
                this.svgMaxAverageSample = svg;
            }

            if (!this.svgMinAverage || averageValues < this.svgMinAverage) {
                this.svgMinAverage = averageValues;
                this.svgMinAverageSample = svg;
            }

            // Add to value's dictionary:
            // Create SVG subunit in dictionary
            if (!this.svgValues[svg]) {
                this.svgValues[svg] = {};
            }
            this.svgValues[svg].average = averageValues;
            this.svgValues[svg].sd = this.#standardDeviationCalc(numValues);

            // Find control value
            const controlData = this.sampleData[whichSVG];
            const controlKeys = Object.keys(controlData.controlComparison);
            if (controlKeys.includes(svg) === false) {
                let controlSampleName = "";
                for (const key of controlKeys) {
                    if (controlData.controlComparison[key].includes(svg)) {
                        controlSampleName = key;
                    }
                }

                if (this.svgValues[controlSampleName] && this.svgValues[controlSampleName].rawValues) {
                    // Calculate control average:
                    const controlValues = this.svgValues[controlSampleName].rawValues;
                    let controlSum = 0;
                    for (const value of controlValues) {
                        controlSum += parseFloat(value);
                    }
                    const controlAverage = controlSum / controlValues.length;

                    let inductionValue = 0;
                    let reductionValue = 0;
                    if (controlAverage !== null && controlAverage > 0 && averageValues > 0) {
                        if (averageValues > controlAverage) {
                            inductionValue = averageValues - controlAverage;
                            this.svgValues[svg].inductionValue = inductionValue;
                        } else if (controlAverage > averageValues) {
                            reductionValue = controlAverage - averageValues;
                            this.svgValues[svg].reductionValue = reductionValue;
                        }

                        const expressionRatio = averageValues / controlAverage;
                        this.svgValues[svg].expressionRatio = expressionRatio;

                        this.svgValues[svg].controlSampleName = controlSampleName;
                        this.svgValues[svg].controlAverage = controlAverage;
                    }
                }
            }
        }

        await this.#colourSVGs(whichSVG, svgSubunits);
    }

    /**
     * Calculate the functional standard deviation
     * Modified from https://www.geeksforgeeks.org/php-program-find-standard-deviation-array/
     * @param {Array} numbers An array of numbers that the standard deviation will be found for
     * @return sd Standard deviation
     */
    // eslint-disable-next-line class-methods-use-this
    #standardDeviationCalc(numbers) {
        let sd = 0;

        const numOfElements = numbers.length;

        if (numOfElements >= 1) {
            let variance = 0.0;

            let numberSum = 0;
            for (let i = 0; i < numOfElements; i += 1) {
                numberSum += numbers[i];
            }
            const average = numberSum / numOfElements;

            for (let x = 0; x < numOfElements; x += 1) {
                variance += (numbers[x] - average) ** 2;
            }

            sd = Math.sqrt(variance / numOfElements);
        }

        return sd;
    }

    /**
     * Colour the existing SVG that has been created
     * @param {String} whichSVG Name of the SVG file without the .svg at the end
     * @param {Array} svgSubunits A list containing all desired SVG subunits to be interacted with
     */
    async #colourSVGs(whichSVG, svgSubunits) {
        for (const subunit of svgSubunits) {
            // Colouring values
            const denominator = this.svgMaxAverage;
            let numerator = this.svgValues[subunit].average;
            if (numerator < 0) {
                numerator = 0;
            }

            let percentage = null;
            if (denominator && denominator >= 0) {
                percentage = (numerator / denominator) * 100;
            }

            if (percentage > 100) {
                percentage = 100;
            } else if (percentage < 0) {
                percentage = 0;
            }
            // Retrieve colouring information
            const colourFill = this.percentageToColour(percentage);

            const expressionLevel = parseFloat(numerator).toFixed(3);
            const sampleSize = this.svgValues[subunit].rawValues.length;

            this.svgValues[subunit].expressionLevel = expressionLevel;
            this.svgValues[subunit].sampleSize = sampleSize;

            // Begin colouring SVG subunits
            // eslint-disable-next-line no-await-in-loop
            await this.#colourSVGsubunit(whichSVG, subunit, colourFill, expressionLevel, sampleSize);
        }
    }

    /**
     * Convert a percentage into a hex-code colour
     * @param {Number} percentage The percentage between 0 - 100 (as an int) into a colour between yellow and red
     * @returns {String} Hex-code colour
     */
    // eslint-disable-next-line class-methods-use-this
    percentageToColour(percentage) {
        const percentageInt = parseInt(percentage, 10);

        if (percentageInt >= 0) {
            // From 0% to 100% as integers
            const colourList = [
                "#ffff00",
                "#fffc00",
                "#fff900",
                "#fff700",
                "#fef400",
                "#fff200",
                "#ffef00",
                "#feed00",
                "#ffea00",
                "#ffe800",
                "#ffe500",
                "#ffe200",
                "#ffe000",
                "#ffdd00",
                "#ffdb00",
                "#ffd800",
                "#ffd600",
                "#fed300",
                "#ffd100",
                "#ffce00",
                "#ffcc00",
                "#ffc900",
                "#ffc600",
                "#ffc400",
                "#ffc100",
                "#ffbf00",
                "#ffbc00",
                "#ffba00",
                "#ffb700",
                "#feb500",
                "#ffb200",
                "#ffaf00",
                "#ffad00",
                "#ffaa00",
                "#ffa800",
                "#ffa500",
                "#ffa300",
                "#ffa000",
                "#ff9e00",
                "#ff9b00",
                "#ff9900",
                "#ff9600",
                "#ff9300",
                "#ff9100",
                "#ff8e00",
                "#ff8c00",
                "#ff8900",
                "#ff8700",
                "#ff8400",
                "#ff8200",
                "#ff7f00",
                "#ff7c00",
                "#ff7a00",
                "#ff7700",
                "#ff7500",
                "#ff7200",
                "#ff7000",
                "#ff6d00",
                "#ff6b00",
                "#ff6800",
                "#ff6600",
                "#ff6300",
                "#ff6000",
                "#ff5e00",
                "#ff5b00",
                "#ff5900",
                "#ff5600",
                "#ff5400",
                "#ff5100",
                "#ff4f00",
                "#ff4c00",
                "#ff4900",
                "#ff4700",
                "#ff4400",
                "#ff4200",
                "#ff3f00",
                "#ff3d00",
                "#ff3a00",
                "#ff3800",
                "#ff3500",
                "#ff3200",
                "#ff3000",
                "#ff2d00",
                "#ff2b00",
                "#ff2800",
                "#ff2600",
                "#ff2300",
                "#ff2100",
                "#ff1e00",
                "#ff1c00",
                "#ff1900",
                "#ff1600",
                "#ff1400",
                "#ff1100",
                "#ff0f00",
                "#ff0c00",
                "#ff0a00",
                "#ff0700",
                "#ff0500",
                "#ff0200",
                "#ff0000",
            ];

            return colourList[percentageInt];
        }
        return "#808080";
    }

    /**
     * The intent is to colour the subunit of a desired location within an SVG
     * @param {String} whichSVG Name of the SVG file without the .svg at the end
     * @param {Array} svgSubunit A list containing all desired SVG subunits to be interacted with
     * @param {String} colour A hex code for what colour it is meant to be filled with
     * @param {Number} expressionLevel The expression level for the interactive data
     * @param {Number} sampleSize The sample size of the input information, default to 1
     */
    async #colourSVGsubunit(whichSVG, svgSubunit, colour, expressionLevel, sampleSize = 1) {
        const svgObject = this.appendSVG.lastElementChild.getElementsByTagName("svg")[0];
        const allParsableElements = [...svgObject.getElementsByTagName("path"), ...svgObject.getElementsByTagName("g")];

        if (svgObject && allParsableElements.length > 0) {
            const expressionData = createSVGExpressionData.svgValues[svgSubunit];
            let descriptionName;
            if (this.sampleData[whichSVG].description) {
                descriptionName = this.sampleData[whichSVG].description[svgSubunit];
            }
            if (descriptionName === undefined || descriptionName === "") {
                descriptionName = svgSubunit;
            }

            // Check for duplicate error:
            const duplicateShoot = [
                "Control_Shoot_0_Hour",
                "Cold_Shoot_0_Hour",
                "Osmotic_Shoot_0_Hour",
                "Salt_Shoot_0_Hour",
                "Drought_Shoot_0_Hour",
                "Genotoxic_Shoot_0_Hour",
                "Oxidative_Shoot_0_Hour",
                "UV-B_Shoot_0_Hour",
                "Wounding_Shoot_0_Hour",
                "Heat_Shoot_0_Hour",
            ];
            const duplicateRoot = [
                "Control_Root_0_Hour",
                "Cold_Root_0_Hour",
                "Osmotic_Root_0_Hour",
                "Salt_Root_0_Hour",
                "Drought_Root_0_Hour",
                "Genotoxic_Root_0_Hour",
                "Oxidative_Root_0_Hour",
                "UV-B_Root_0_Hour",
                "Wounding_Root_0_Hour",
                "Heat_Root_0_Hour",
            ];
            let isdupShoot = false;
            let isdupRoot = false;
            if (duplicateShoot.includes(svgSubunit)) {
                isdupShoot = true;
            } else if (duplicateRoot.includes(svgSubunit)) {
                isdupRoot = true;
            }

            let subunitElement;
            for (const i in allParsableElements) {
                if (allParsableElements[i].id === svgSubunit) {
                    subunitElement = allParsableElements[i];
                }
            }

            // This is used to determine if the SVG should be automatically coloured or manually done
            if (subunitElement && subunitElement.childNodes.length > 0) {
                const childElements = subunitElement.childNodes;

                let coloured = false;

                for (const c in childElements) {
                    if (childElements[c].tagName === "path" || childElements[c].tagName === "g") {
                        childElements[c].setAttribute("fill", colour);

                        coloured = true;
                    }
                }

                if (!coloured) {
                    subunitElement.setAttribute("fill", colour);
                }
            } else if (subunitElement) {
                subunitElement.setAttribute("fill", colour);
            }

            // Add interactivity
            if (subunitElement) {
                // Adding hover features:
                subunitElement.setAttribute("class", "hoverDetails");
                subunitElement.addEventListener(
                    "mouseenter",
                    // eslint-disable-next-line func-names
                    (event) => {
                        window.requestAnimationFrame(() => {
                            debounceTissueMetadata(addTissueMetadata(event.target.id), 250);
                        });
                    },
                );
                subunitElement.addEventListener(
                    "mouseleave",
                    // eslint-disable-next-line func-names
                    (event) => {
                        window.requestAnimationFrame(() => {
                            debounceTissueMetadata(removeTissueMetadata(event.target.id), 250);
                        });
                    },
                );
                // Adding details about sub-tissue:
                subunitElement.setAttribute("data-expressionValue", expressionLevel);
                subunitElement.setAttribute("data-sampleSize", sampleSize);
                subunitElement.setAttribute("data-standardDeviation", expressionData.sd);
                subunitElement.setAttribute("data-sampleSize", sampleSize);

                // Add tooltip/title on hover
                const title = document.createElementNS("https://www.w3.org/2000/svg", "title");
                if (title) {
                    title.textContent = `${descriptionName}\r\nExpression level: ${expressionLevel}\r\nSample size: ${sampleSize}\r\nStandard Deviation: ${parseFloat(
                        expressionData.sd,
                    ).toFixed(3)}`;
                }

                // Add rest of titles and tooltip/title
                const inducReduc = false;

                // if (expressionData['inductionValue']) {
                //     subunitElement.setAttribute("data-inductionValue", expressionData['inductionValue']);
                //     title.textContent += '\r\nInduction Value: ' + parseFloat(expressionData['inductionValue']).toFixed(3);
                //     inducReduc = true;
                // } else if (expressionData['reductionValue']) {
                //     subunitElement.setAttribute("data-reductionValue", expressionData['reductionValue']);
                //     title.textContent += '\r\nReduction Value: ' + parseFloat(expressionData['ReductionValue']).toFixed(3);
                //     inducReduc = true;
                // };

                if (inducReduc === true) {
                    subunitElement.setAttribute("data-expressionRatio", expressionData.expressionRatio);
                    title.textContent += `\r\nExpression Ratio: ${parseFloat(expressionData.expressionRatio).toFixed(
                        3,
                    )}`;
                    subunitElement.setAttribute("data-controlSampleName", expressionData.controlSampleName);

                    let controlSampleName;
                    if (this.sampleData[whichSVG].description) {
                        controlSampleName = this.sampleData[whichSVG].description[expressionData.controlSampleName];
                    }
                    if (controlSampleName === undefined || controlSampleName === "") {
                        controlSampleName = expressionData.controlSampleName;
                    }
                    title.textContent += `\r\nControl Sample Name: ${controlSampleName}`;

                    subunitElement.setAttribute("data-controlAverage", expressionData.controlAverage);
                    title.textContent += `\r\nControl Expression: ${parseFloat(expressionData.controlAverage).toFixed(
                        3,
                    )}`;
                }
                subunitElement.appendChild(title);
            }

            // Correcting duplicate error:
            if (isdupShoot) {
                for (const dupS in duplicateShoot) {
                    if (duplicateShoot[dupS]) {
                        // Find duplicate shoot element:
                        let dupShootElement;
                        for (const i in allParsableElements) {
                            if (allParsableElements[i].id === duplicateShoot[dupS]) {
                                dupShootElement = allParsableElements[i];
                            }
                        }

                        if (dupShootElement) {
                            // Add interactivity
                            dupShootElement.setAttribute("class", "hoverDetails");
                            dupShootElement.addEventListener(
                                "mouseenter",
                                // eslint-disable-next-line func-names
                                function (_event) {
                                    addTissueMetadata(this.id);
                                },
                                { passive: true },
                            );
                            dupShootElement.addEventListener(
                                "mouseleave",
                                // eslint-disable-next-line func-names
                                function (_event) {
                                    removeTissueMetadata(this.id);
                                },
                                { passive: true },
                            );
                            // Adding colour
                            const childElements = dupShootElement.childNodes;
                            if (childElements.length > 0) {
                                let foundColor = false;

                                for (const element of childElements) {
                                    if (element.tagName === "path") {
                                        element.setAttribute("fill", colour);
                                        foundColor = true;
                                    }
                                }

                                if (!foundColor) {
                                    dupShootElement.setAttribute("fill", colour);
                                }
                            } else {
                                dupShootElement.setAttribute("fill", colour);
                            }
                            // Add tooltip/title on hover
                            const title = document.createElementNS("https://www.w3.org/2000/svg", "title");
                            if (title) {
                                title.textContent = `${duplicateShoot[dupS]}\r\nExpression level: ${expressionLevel}\r\nSample size: ${sampleSize}`;
                                dupShootElement.appendChild(title);
                            }
                        }
                    }
                }
            } else if (isdupRoot) {
                for (const dupR in duplicateRoot) {
                    if (duplicateRoot[dupR]) {
                        let dupRootElement;
                        for (const i in allParsableElements) {
                            if (allParsableElements[i].id === duplicateRoot[dupR]) {
                                dupRootElement = allParsableElements[i];
                            }
                        }

                        if (dupRootElement) {
                            // Add interactivity
                            dupRootElement.setAttribute("class", "hoverDetails");
                            dupRootElement.addEventListener(
                                "mouseenter",
                                // eslint-disable-next-line func-names
                                function (_event) {
                                    addTissueMetadata(this.id);
                                },
                                { passive: true },
                            );
                            dupRootElement.addEventListener(
                                "mouseleave",
                                // eslint-disable-next-line func-names
                                function (_event) {
                                    removeTissueMetadata(this.id);
                                },
                                { passive: true },
                            );
                            // Adding colour
                            const childElements = dupRootElement.childNodes;
                            if (childElements.length > 0) {
                                let foundColor = false;

                                for (const element of childElements) {
                                    if (element.tagName === "path") {
                                        element.setAttribute("fill", colour);
                                        foundColor = true;
                                    }
                                }

                                if (!foundColor) {
                                    dupRootElement.setAttribute("fill", colour);
                                }
                            } else {
                                dupRootElement.setAttribute("fill", colour);
                            }
                            // Add tooltip/title on hover
                            const title = document.createElementNS("https://www.w3.org/2000/svg", "title");
                            if (title) {
                                title.textContent = `${duplicateRoot[dupR]}\r\nExpression level: ${expressionLevel}\r\nSample size: ${sampleSize}`;
                                dupRootElement.appendChild(title);
                            }
                        }
                    }
                }
            }
        }
    }
}
/**
 * Create and retrieve expression data in an SVG format
 */
const createSVGExpressionData = new CreateSVGExpressionData();
window.createSVGExpressionData = createSVGExpressionData;