function Right_Content() {}

Right_Content.prototype.init = function () {
    var self = this;
    self.dom_node = $("#right-content");

    // Download
    // Set up events for downloading object
    var saveObjectModal = $("#saveObjectModal");

    self.download_button = $("#export-object-button");
    // https://stackoverflow.com/a/42830315
    function downloadFile(urlToSend, file_name) {
        var req = new XMLHttpRequest();
        req.open("GET", urlToSend, true);
        req.responseType = "blob";
        req.onload = function (event) {
            var blob = req.response;
            var fileName = file_name + "_" + req.getResponseHeader("filename");
            var link = document.createElement("a");
            link.href = window.URL.createObjectURL(blob);
            link.download = fileName;
            link.click();
        };

        req.send();
    }

    // https://stackoverflow.com/a/30800715
    function downloadObjectAsJson(exportObj, exportName) {
        var dataStr =
            "data:text/json;charset=utf-8," +
            encodeURIComponent(JSON.stringify(exportObj));
        var downloadAnchorNode = document.createElement("a");
        downloadAnchorNode.setAttribute("href", dataStr);
        downloadAnchorNode.setAttribute("download", exportName + ".json");
        document.body.appendChild(downloadAnchorNode); // required for firefox
        downloadAnchorNode.click();
        downloadAnchorNode.remove();
    }

    saveObjectModal.find(".confirm-button").on("click", function () {
        var prefix = $("#saveObjectName").val();
        // selections call
        downloadFile("/Download/Selections", prefix);
        // de call
        downloadFile("/Download/DE", prefix);
        // download the current scatter plot's data
        var scatter_json = _.cloneDeep(right_content.getSelectedPlotData());
        // included the selected cells
        scatter_json["selected_cells"] = get_global_status("selected_cell");
        delete scatter_json["scatter"];
        downloadObjectAsJson(scatter_json, prefix + "_scatter");
        saveObjectModal.modal("hide");
    });

    // End download button

    self.scatterColorOptions = $(self.dom_node).find(
        "input[name='scatterColorButtons']",
    );
    self.scatterLayoutOptions = $(self.dom_node).find(
        "input[name='scatterLayoutButtons']",
    );

    self.selectedLayout = "full";
    self.syncCheck = $(this.dom_node).find("#sync-check").get(0);
    self.treeView = false;

    // Holds cached plot data -- needed when redrawing all plots
    self.layoutPlotData = {
        full: [{}],
        splitH: [{}, {}],
        splitV: [{}, {}],
        split4: [{}, {}, {}, {}],
    };

    self.layoutMeta = {
        full: {
            selectedPlot: 0,
            initialized: true,
            plotDivs: ["#scatter-div"],
        },
        splitH: {
            selectedPlot: 0,
            initialized: false,
            plotDivs: ["#scatter-splitH-div0", "#scatter-splitH-div1"],
        },
        splitV: {
            selectedPlot: 0,
            initialized: false,
            plotDivs: ["#scatter-splitV-div0", "#scatter-splitV-div1"],
        },
        split4: {
            selectedPlot: 0,
            initialized: false,
            plotDivs: [
                "#scatter-split-div0",
                "#scatter-split-div1",
                "#scatter-split-div2",
                "#scatter-split-div3",
            ],
        },
    };

    self.layoutPlotData["full"][0]["scatter"] = new ColorScatter(
        "#scatter-div",
    );

    $(self.scatterColorOptions).on("change", function () {
        self.update({ colorScatterOption: "" }); // No need to send value
    });

    $(self.scatterLayoutOptions).on("change", function (eventObj) {
        var selectedValue = eventObj.currentTarget.value;

        $(self.dom_node).find(".scatter-layout-container").hide();

        if (selectedValue === "full") {
            $(self.dom_node).find("#scatter-div").show();
        } else if (selectedValue === "splitH") {
            $(self.dom_node).find("#scatter-splitH-div").show();
        } else if (selectedValue === "splitV") {
            $(self.dom_node).find("#scatter-splitV-div").show();
        } else if (selectedValue === "split4") {
            $(self.dom_node).find("#scatter-split-div").show();
        }

        self.changeLayout(selectedValue);
    });

    var _scatter_relayout = function (e) {
        // Used to synchronize zoom events between visible scatter plots
        // First need to remove the listener to prevent infinite loop

        self.dom_node
            .get(0)
            .removeEventListener("scatter_relayout", _scatter_relayout);

        var promises = [];

        var visiblePlotData = self.getVisiblePlotData();
        for (var i = 0; i < visiblePlotData.length; i++) {
            if (
                e.detail.origin !== visiblePlotData[i]["scatter"] &&
                e.detail.projKeyX[0] ===
                    visiblePlotData[i]["projection_keyX"][0] && // Only sync zoom if same plot
                e.detail.projKeyX[1] ===
                    visiblePlotData[i]["projection_keyX"][1] &&
                e.detail.projKeyY[0] ===
                    visiblePlotData[i]["projection_keyY"][0] &&
                e.detail.projKeyY[1] ===
                    visiblePlotData[i]["projection_keyY"][1]
            ) {
                promises.push(
                    visiblePlotData[i]["scatter"].relayout(e.detail.newLayout),
                );
            }
        }

        Promise.all(promises).then(function () {
            self.dom_node
                .get(0)
                .addEventListener("scatter_relayout", _scatter_relayout);
        });
    };

    self.dom_node
        .get(0)
        .addEventListener("scatter_relayout", _scatter_relayout);

    // Allow for clicking to specify selected plot div
    $(self.dom_node)
        .find(".scatter-split-plot-div")
        .on("click", function (e) {
            var targetId = e.currentTarget.id;
            var layoutMeta = self.layoutMeta[self.selectedLayout];
            layoutMeta["selectedPlot"] = parseInt(
                targetId[targetId.length - 1],
            );

            // Need to clear current selection, but only inside the current scatter-layout-container
            $(e.currentTarget)
                .parents(".scatter-layout-container")
                .find(".scatter-split-plot-div")
                .removeClass("active");

            $(e.currentTarget).addClass("active");

            // Fire the update so the lower-left area changes too
            var plottedData = self.getSelectedPlotData();

            self.syncSelectedProj();

            // Fire the update so the lower-left area changes too
            var update = {};
            update["plotted_item_type"] = plottedData["item_type"];
            update["plotted_item"] = plottedData["item_key"];

            set_global_status(update);
        });

    // Set up events for saving/loading selections
    var saveSelectionModal = $("#saveSelectionModal");
    saveSelectionModal
        .on("shown.bs.modal", function () {
            // Update selection name if it has a name already
            var selectionName = get_global_status("selection_name");
            var selectionInput = $(this).find("#saveSelectionName");
            if (selectionName !== "Selection") {
                selectionInput.val(selectionName);
            } else {
                selectionInput.val("");
            }
            selectionInput.focus();
        })
        .on("keyup", function (event) {
            if (event.key != "Enter") return;
            $(this).find(".confirm-button").click();
            event.preventDefault();
        });

    saveSelectionModal.find(".confirm-button").on("click", function () {
        // Save selection and close modal
        var selectionName = saveSelectionModal.find("#saveSelectionName").val();

        if (selectionName === "") return;

        var selectedCells = get_global_status("selected_cell");

        api.cells.saveSelection(selectionName, selectedCells).then(() => {
            set_global_status({
                selections_changed: true,
            });
        });

        set_global_status({
            selection_name: selectionName,
        });
        saveSelectionModal.modal("hide");
    });

    var loadSelectionModal = $("#loadSelectionModal");
    var lSM_loading = createLoadingFunction(
        loadSelectionModal.find(".modal-body"),
    );
    loadSelectionModal
        .on("show.bs.modal", function () {
            // Load selections and display

            var modalList = loadSelectionModal.find(".selection-list");
            var emptyList = loadSelectionModal.find(".empty-list");
            lSM_loading(true);

            api.cells
                .listSelections()
                .then((data) => {
                    modalList.empty();

                    var confirmButton =
                        loadSelectionModal.find(".confirm-button");
                    if (data.length === 0) {
                        modalList.hide();
                        emptyList.show();
                        confirmButton.attr("disabled", true);
                        return;
                    }
                    emptyList.hide();
                    modalList.show();

                    confirmButton.attr("disabled", false);

                    var itemClickFun = function () {
                        modalList
                            .find(".loadable-selection-option")
                            .removeClass("selected");
                        $(this).addClass("selected");
                    };

                    _.each(data, (name, i) => {
                        var e = document.createElement("div");
                        e.innerText = name;
                        e.classList.add("loadable-selection-option");
                        if (i == 0) {
                            // Select first element
                            e.classList.add("selected");
                        }
                        e.addEventListener("click", itemClickFun);
                        modalList.append(e);
                    });
                })
                .always(() => {
                    lSM_loading(false);
                });
        })
        .on("keyup", function (event) {
            if (event.key != "Enter") return;
            $(this).find(".confirm-button").click();
            event.preventDefault();
        });

    loadSelectionModal.find(".confirm-button").on("click", function () {
        // Load selection and close modal
        self.setLoadingStatus(true);

        var selectionToLoad = loadSelectionModal
            .find(".loadable-selection-option.selected")
            .text();

        api.cells.getSelection(selectionToLoad).then((cells) => {
            var update = {
                selected_cell: cells,
                selection_type: get_global_status("pooled") ? "pools" : "cells",
                selection_name: selectionToLoad,
            };
            set_global_status(update);
        });

        loadSelectionModal.modal("hide");
    });

    self.setLoadingStatus = createLoadingFunction(self.dom_node);

    var proj_promise = api.projections.list().then(function (proj_names) {
        self.proj_names_scatter = proj_names;
        var projSelect = self.dom_node.find("#SelectProjScatter");
        var projSelectX = self.dom_node.find("#SelectProjScatterX");
        var projSelectY = self.dom_node.find("#SelectProjScatterY");
        projSelect.children().remove();

        _.each(proj_names, function (val, key) {
            projSelect.append(
                $("<option>", {
                    value: key,
                    text: key,
                }),
            );
        });

        var update_axes_dropdowns = function () {
            var selected_proj = $(projSelect).val();

            projSelectX.children().remove();
            projSelectY.children().remove();

            _.each(proj_names[selected_proj], function (val) {
                projSelectX.append(
                    $("<option>", {
                        value: val,
                        text: val,
                    }),
                );
                projSelectY.append(
                    $("<option>", {
                        value: val,
                        text: val,
                    }),
                );
            });

            $(projSelectX).val(proj_names[selected_proj][0]);
            $(projSelectY).val(proj_names[selected_proj][1]);
            projSelectX.trigger("chosen:updated");
            projSelectY.trigger("chosen:updated");
        };

        projSelectX
            .chosen({
                width: "150px",
                disable_search_threshold: 99,
            })
            .off("change")
            .on("change", function () {
                set_global_status({
                    plotted_projectionX: [
                        $(projSelect).val(),
                        $(projSelectX).val(),
                    ],
                    plotted_projectionY: [
                        $(projSelect).val(),
                        $(projSelectY).val(),
                    ],
                });
            });

        projSelectY
            .chosen({
                width: "150px",
                disable_search_threshold: 99,
            })
            .off("change")
            .on("change", function () {
                set_global_status({
                    plotted_projectionX: [
                        $(projSelect).val(),
                        $(projSelectX).val(),
                    ],
                    plotted_projectionY: [
                        $(projSelect).val(),
                        $(projSelectY).val(),
                    ],
                });
            });

        projSelect
            .chosen({
                width: "150px",
                disable_search_threshold: 99,
            })
            .off("change")
            .on("change", function () {
                update_axes_dropdowns();
                set_global_status({
                    plotted_projectionX: [$(this).val(), $(projSelectX).val()],
                    plotted_projectionY: [$(this).val(), $(projSelectY).val()],
                });
            })
            .trigger("chosen:updated");

        update_axes_dropdowns();
    });

    var treeproj_promise = api.tree.list().then(function (proj_names) {
        self.proj_names_trajectory = proj_names;
        var projSelect = self.dom_node.find("#SelectTrajectoryProjScatter");
        projSelect.children().remove();

        _.each(proj_names, function (proj) {
            projSelect.append(
                $("<option>", {
                    value: proj,
                    text: proj,
                }),
            );
        });

        projSelect
            .chosen({
                width: "110px",
                disable_search_threshold: 99,
            })
            .off("change")
            .on("change", function () {
                set_global_status({
                    plotted_trajectory: $(this).val(),
                });
            })
            .trigger("chosen:updated");
    });

    return $.when(proj_promise, treeproj_promise);
};

Right_Content.prototype.update = function (updates) {
    var self = this;

    var plotData = self.getSelectedPlotData();

    // Update the selection status in ALL plots
    if ("selected_cell" in updates) {
        var visiblePlotData = self.getVisiblePlotData();
        for (var i = 0; i < visiblePlotData.length; i++) {
            // Need to check in case it's not initialized
            if ("scatter" in visiblePlotData[i]) {
                visiblePlotData[i]["scatter"].updateSelection();
            }
        }

        var selection_type = get_global_status("selection_type");
        var selection_button = $(self.dom_node).find("#save-selection-button");
        if (selection_type === "cells" || selection_type === "pools") {
            selection_button.attr("disabled", false);
        } else {
            selection_button.attr("disabled", true);
        }
    }

    // Show either the Projection or Tree lists
    var treeViewChanged = false;

    if ("main_vis" in updates) {
        var main_vis = get_global_status("main_vis");

        $("#plot-subtitle-latent").hide();
        $("#plot-subtitle-trajectory").hide();
        if (main_vis === "tree") {
            $("#plot-subtitle-trajectory").show();
            treeViewChanged = self.treeView === false;
            self.treeView = true;
        } else {
            $("#plot-subtitle-latent").show();
            treeViewChanged = self.treeView === true;
            self.treeView = false;
        }
    }

    if ("colorScatterOption" in updates) {
        // Mark all plots for update
        var allPlotData = self.getAllPlotData();
        for (var i = 0; i < allPlotData.length; i++) {
            allPlotData[i]["needsUpdate"] = true;
        }
    }

    // If this, then we just need to change the selected visualization
    if (
        "plotted_item" in updates ||
        "plotted_item_type" in updates ||
        "plotted_values" in updates
    ) {
        var item_key = get_global_status("plotted_item");
        var item_type = get_global_status("plotted_item_type");
        var values = get_global_data("plotted_values");

        plotData["item_key"] = item_key;
        plotData["item_type"] = item_type;
        plotData["values"] = values;
        plotData["needsUpdate"] = true;
    }

    var autoZoom = false;

    var update_promise;

    if (
        "plotted_projectionX" in updates ||
        "plotted_projectionY" in updates ||
        "plotted_trajectory" in updates ||
        treeViewChanged
    ) {
        autoZoom = true;
        var proj_promise;

        if (
            !self.treeView &&
            ("plotted_projectionX" in updates ||
                "plotted_projectionY" in updates ||
                treeViewChanged)
        ) {
            var proj_keyX = get_global_status("plotted_projectionX");
            var proj_keyY = get_global_status("plotted_projectionY");
            var proj_promiseX = api.projections.coordinates(
                proj_keyX[0],
                proj_keyX[1],
            );
            var proj_promiseY = api.projections.coordinates(
                proj_keyY[0],
                proj_keyY[1],
            );
            proj_promise = $.when(proj_promiseX, proj_promiseY).then(
                function (projectionX, projectionY) {
                    var keys = _.keys(projectionX);
                    var values = _.map(keys, (key) => [
                        projectionX[key],
                        projectionY[key],
                    ]);
                    var projection = _.zipObject(keys, values);
                    return {
                        projection: projection,
                        projection_type: "default",
                        projection_keyX: proj_keyX,
                        projection_keyY: proj_keyY,
                    };
                },
            );
        } else if (
            self.treeView &&
            ("plotted_trajectory" in updates || treeViewChanged)
        ) {
            var proj_key = get_global_status("plotted_trajectory");
            proj_promise = api.tree
                .coordinates(proj_key)
                .then(function (projection) {
                    return {
                        projection: projection,
                        projection_type: "tree",
                        projection_keyX: [proj_key, ""],
                        projection_keyY: [proj_key, ""],
                    };
                });
        }

        update_promise = proj_promise.then(function (data) {
            var projection = data["projection"];
            var projection_type = data["projection_type"];
            var projection_keyX = data["projection_keyX"];
            var projection_keyY = data["projection_keyY"];

            // If the projection is changing, then we need to change all the plots
            // Alternately, if we switch the view (e.g. from Trajectories to Scatter)
            // then we change them all
            var synchronized = $(self.syncCheck).is(":checked");
            if (synchronized || treeViewChanged) {
                var allPlotData = self.getAllPlotData();
                for (var i = 0; i < allPlotData.length; i++) {
                    allPlotData[i]["projection"] = projection;
                    allPlotData[i]["projection_type"] = projection_type;
                    allPlotData[i]["projection_keyX"] = projection_keyX;
                    allPlotData[i]["projection_keyY"] = projection_keyY;
                    allPlotData[i]["needsUpdate"] = true;
                }
            } else {
                var selectedPlotData = self.getSelectedPlotData();
                selectedPlotData["projection"] = projection;
                selectedPlotData["projection_type"] = projection_type;
                selectedPlotData["projection_keyX"] = projection_keyX;
                selectedPlotData["projection_keyY"] = projection_keyY;
                selectedPlotData["needsUpdate"] = true;
            }
        });
    } else {
        update_promise = $.Deferred().resolve().promise();
    }

    return update_promise.then(function () {
        // Update all visible plots if they need it
        var visiblePlotData = self.getVisiblePlotData();
        for (i = 0; i < visiblePlotData.length; i++) {
            if (visiblePlotData[i]["needsUpdate"]) {
                self.draw_scatter(visiblePlotData[i], autoZoom);
            }
        }
    });
};

Right_Content.prototype.select_default_proj = function () {
    var proj_name = $(this.dom_node.find("#SelectProjScatter")).val();
    var proj_X = $(this.dom_node.find("#SelectProjScatterX")).val();
    var proj_Y = $(this.dom_node.find("#SelectProjScatterY")).val();
    var traj = $(this.dom_node.find("#SelectTrajectoryProjScatter")).val();

    var update = {};
    update["plotted_projectionX"] = [proj_name, proj_X];
    update["plotted_projectionY"] = [proj_name, proj_Y];
    update["plotted_trajectory"] = traj;
    return update;
};

/*
 * This is used to make sure that changing the selected projection
 * updates the dropdown menus up top accordingly
 */
Right_Content.prototype.syncSelectedProj = function () {
    var self = this;
    var plottedData = self.getSelectedPlotData();
    var proj_keyX = plottedData["projection_keyX"];
    var proj_keyY = plottedData["projection_keyY"];

    if (proj_keyX[0] in self.proj_names_scatter) {
        var projSelect = self.dom_node.find("#SelectProjScatter");
        var projSelectX = self.dom_node.find("#SelectProjScatterX");
        var projSelectY = self.dom_node.find("#SelectProjScatterY");
        $(projSelect).val(proj_keyX[0]);

        projSelectX.children().remove();
        projSelectY.children().remove();

        _.each(self.proj_names_scatter[proj_keyX[0]], function (val) {
            projSelectX.append(
                $("<option>", {
                    value: val,
                    text: val,
                }),
            );
            projSelectY.append(
                $("<option>", {
                    value: val,
                    text: val,
                }),
            );
        });

        $(projSelectX).val(proj_keyX[1]);
        $(projSelectY).val(proj_keyY[1]);
        projSelect.trigger("chosen:updated");
        projSelectX.trigger("chosen:updated");
        projSelectY.trigger("chosen:updated");
    } else {
        var projSelect = self.dom_node.find("#SelectTrajectoryProjScatter");
        $(projSelect).val(proj_keyX[0]);
        projSelect.trigger("chosen:updated");
    }
};

/*
 * scatter: Instance of ColorScatter - which plot to update
 * item_key: string - name of plotted item
 * item_type: string - one of 'signature', 'meta', 'gene', or 'signature-gene'
 * values: sample name (str) -> int/str - values to plot
 * projection: sample name (str) -> [x, y] - coordinates for each sample
 * autoZoom: bool - whether or not to re-zoom the plot
 */
Right_Content.prototype.draw_sigvp = function (plotData, autoZoom) {
    var self = this;
    var scatter = plotData["scatter"];
    var item_key = plotData["item_key"];
    var item_type = plotData["item_type"];
    var values = plotData["values"];
    var projection = plotData["projection"];
    var proj_keyX = plotData["projection_keyX"];
    var proj_keyY = plotData["projection_keyY"];

    var sample_value = _.values(values)[0];
    var isFactor = typeof sample_value === "string" && sample_value !== "NA";

    var full_color_range, diverging_colormap;
    if (item_type === "gene" || item_type === "signature-gene") {
        full_color_range = true;
        diverging_colormap = false;
    } else if (item_type === "meta") {
        full_color_range = false;
        diverging_colormap = true;
    } else {
        full_color_range = false;
        diverging_colormap = true;
    }

    if (self.getScatterColorOption() == "rank" && item_type === "signature") {
        values = self.rank_values(values);
    }

    var points = [];
    var sample_labels = Object.keys(values).sort();
    var selected_cells = get_global_status("selected_cell");
    var selected_cells_map = _.keyBy(selected_cells, (x) => x);
    if (selected_cells.length == 1) {
        // Just a single cell
        selected_cells_map = {}; // Don't style anything
    }

    _.each(sample_labels, (sample_label) => {
        var x = projection[sample_label][0];
        var y = projection[sample_label][1];
        var sig_score = values[sample_label];
        var selected = sample_label in selected_cells_map;

        points.push({
            x: x,
            y: y,
            value: sig_score,
            label: sample_label,
            selected: selected,
        });
    });

    scatter.setData({
        points: points,
        isFactor: isFactor,
        full_color_range: full_color_range,
        diverging_colormap: diverging_colormap,
        title: item_key,
        autozoom: autoZoom,
        proj_keyX: proj_keyX,
        proj_keyY: proj_keyY,
    });
};

/*
 * scatter: Instance of ColorScatter - which plot to update
 * item_key: string - name of plotted item
 * item_type: string - one of 'signature', 'meta', 'gene', or 'signature-gene'
 * values: sample name (str) -> int/str - values to plot
 * projection: list
 *             [0]: sample name (str) -> [x, y] - coordinates for each sample
 *             [1]: Milestone coordiantes (list), [vData, adjMat]
 * autoZoom: bool - whether or not to re-zoom the plot
 */
Right_Content.prototype.draw_tree = function (plotData, autoZoom) {
    var self = this;

    var scatter = plotData["scatter"];
    var item_key = plotData["item_key"];
    var item_type = plotData["item_type"];
    var values = plotData["values"];
    var projection = plotData["projection"];
    var proj_keyX = plotData["projection_keyX"];
    var proj_keyY = plotData["projection_keyY"];

    var isFactor =
        typeof _.values(values)[0] === "string" && _.values(values)[0] !== "NA";

    var full_color_range, diverging_colormap;
    if (item_type === "gene" || item_type === "signature-gene") {
        full_color_range = true;
        diverging_colormap = false;
    } else if (item_type === "meta") {
        full_color_range = false;
        diverging_colormap = true;
    } else {
        full_color_range = false;
        diverging_colormap = true;
    }

    if (
        self.getScatterColorOption() == "rank" &&
        !isFactor &&
        item_type !== "gene" &&
        item_type !== "meta"
    ) {
        values = self.rank_values(values);
    }

    var milestoneCoordinates = projection[1];
    var projection = projection[0];

    var treep = milestoneCoordinates[0];
    var treel = milestoneCoordinates[1];

    // Massage treep for easier D3 binding

    tree_points = [];

    var points = [];
    var sample_labels = Object.keys(values).sort();
    var selected_cells = get_global_status("selected_cell");
    var selected_cells_map = _.keyBy(selected_cells, (x) => x);
    if (selected_cells.length == 1) {
        // Just a single cell
        selected_cells_map = {}; // Don't style anything
    }

    _.each(sample_labels, (sample_label) => {
        var x = projection[sample_label][0];
        var y = projection[sample_label][1];
        var sig_score = values[sample_label];
        var selected = sample_label in selected_cells_map;

        points.push({
            x: x,
            y: y,
            value: sig_score,
            label: sample_label,
            selected: selected,
        });
    });

    var tree_points = [];
    for (var i = 0; i < treep.length; i++) {
        var x = treep[i][0];
        var y = treep[i][1];
        tree_points.push([x, y, "Node " + i]);
    }

    // Change tree adjacency list into a list of pairs
    var tree_adj = [];

    for (var i = 0; i < treel.length; i++) {
        for (var j = i + 1; j < treel[i].length; j++) {
            if (treel[i][j] == 1) {
                tree_adj.push([i, j]);
            }
        }
    }

    scatter.setData({
        points: points,
        isFactor: isFactor,
        full_color_range: full_color_range,
        diverging_colormap: diverging_colormap,
        tree_points: tree_points,
        tree_adj: tree_adj,
        title: item_key,
        autozoom: autoZoom,
        proj_keyX: proj_keyX,
        proj_keyY: proj_keyY,
    });
};

// Called when the window is resized
Right_Content.prototype.resize = function () {
    var visiblePlots = this.getVisiblePlotData();

    for (var i = 0; i < visiblePlots.length; i++) {
        visiblePlots[i]["scatter"].resize();
    }
};

Right_Content.prototype.getScatterColorOption = function () {
    return this.scatterColorOptions.filter(":checked").val();
};

//Returns the rank of each value in values (object)
//Averages ranks that are ties
Right_Content.prototype.rank_values = function (values) {
    var pairs = _.toPairs(values);
    pairs.sort(function (a, b) {
        return a[1] - b[1];
    });
    var ranks = {};

    var current_group_start = 0;
    var current_group_end;
    var last_val = pairs[0][1];
    var current_val;
    var current_group_rank;
    var i, j;

    for (i = 1; i < pairs.length; i++) {
        current_val = pairs[i][1];
        if (current_val !== last_val) {
            current_group_end = i - 1;
            current_group_rank = (current_group_end + current_group_start) / 2;
            for (j = current_group_start; j <= current_group_end; j++) {
                ranks[pairs[j][0]] = current_group_rank;
            }
            current_group_start = i;
        }
        last_val = current_val;
    }

    // Need to wrap up the final group
    current_group_end = pairs.length - 1;
    current_group_rank = (current_group_end + current_group_start) / 2;
    for (j = current_group_start; j <= current_group_end; j++) {
        ranks[pairs[j][0]] = current_group_rank;
    }

    return ranks;
};

Right_Content.prototype.hover_cells = function (cell_ids) {
    var visiblePlots = this.getVisiblePlotData();

    for (var i = 0; i < visiblePlots.length; i++) {
        visiblePlots[i]["scatter"].hover_cells(cell_ids);
    }
};

Right_Content.prototype.changeLayout = function (newLayout) {
    // Old plot data
    var refPlotData = this.getSelectedPlotData();
    var refVisiblePlotData = this.getVisiblePlotData();

    // Change layout here
    this.selectedLayout = newLayout;
    var newLayoutMeta = this.layoutMeta[this.selectedLayout];

    var splitInitialized = newLayoutMeta["initialized"];

    var plotData, visiblePlotData;
    if (splitInitialized) {
        plotData = this.getSelectedPlotData();

        for (var prop in refPlotData) {
            if (prop === "scatter") {
                continue;
            }
            if (prop === "needsUpdate") {
                continue;
            }
            plotData[prop] = refPlotData[prop];
        }
    } else {
        // New plot data
        visiblePlotData = this.getVisiblePlotData();
        var plotDivs = newLayoutMeta["plotDivs"];

        // Loop through visible splits assigning their content from exists splits
        for (var i = 0; i < visiblePlotData.length; i++) {
            plotData = visiblePlotData[i];
            refPlotData = refVisiblePlotData[i % refVisiblePlotData.length];

            plotData["scatter"] = new ColorScatter(plotDivs[i]);
            plotData["needsUpdate"] = true;

            for (prop in refPlotData) {
                if (prop === "scatter") {
                    continue;
                }
                if (prop === "needsUpdate") {
                    continue;
                }
                plotData[prop] = refPlotData[prop];
            }
        }

        newLayoutMeta["initialized"] = true;
    }

    visiblePlotData = this.getVisiblePlotData();

    // Need to purge all non-visible plots to reduce WebGL context count
    // Then need to re-create all visible plots
    var allPlotData = this.getAllPlotData();
    for (var i = 0; i < allPlotData.length; i++) {
        if ("scatter" in allPlotData[i]) {
            allPlotData[i]["scatter"].clear();
        }
    }

    var autoZoom = false;
    for (var i = 0; i < visiblePlotData.length; i++) {
        $.extend(
            visiblePlotData[i]["scatter"].currentZoom,
            refPlotData["scatter"].currentZoom,
        );
        this.draw_scatter(visiblePlotData[i], autoZoom);
    }
};

Right_Content.prototype.draw_scatter = function (plotData, autoZoom) {
    if (plotData["projection_type"] === "default") {
        this.draw_sigvp(plotData, autoZoom);
    } else if (plotData["projection_type"] === "tree") {
        this.draw_tree(plotData, autoZoom);
    } else {
        throw "Bad projection_type value!";
    }

    plotData["needsUpdate"] = false;
};

Right_Content.prototype.getSelectedPlotData = function () {
    var layoutMeta = this.layoutMeta[this.selectedLayout];
    var visiblePlotData = this.layoutPlotData[this.selectedLayout];

    return visiblePlotData[layoutMeta.selectedPlot];
};

Right_Content.prototype.getVisiblePlotData = function () {
    var visiblePlotData = this.layoutPlotData[this.selectedLayout];
    return visiblePlotData;
};

Right_Content.prototype.getAllPlotData = function () {
    return _.flatMap(this.layoutPlotData, (x) => _.values(x));
};

Right_Content.prototype.getAllLayoutMeta = function () {
    return _.flatMap(this.layoutMeta, (x) => _.values(x));
};
