grnsight
Version:
Web app and service for visualizing models of gene regulatory networks
633 lines (538 loc) • 22 kB
JavaScript
import { updaters } from "./graph";
import { updateApp, identifySpeciesMenu } from "./update-app";
import { saveSvgAsPng } from "save-svg-as-png";
import * as jsPDF from "jspdf";
import canvg from "canvg";
import {
HOST_SITE,
FORCE_GRAPH,
GRID_LAYOUT,
SET_NORMALIZATION_SIDEBAR,
SET_NORMALIZATION_SIDEBAR_VALUE,
SET_NORMALIZATION_MENU,
RESET_NORMALIZATION_SIDEBAR,
RESET_NORMALIZATION_MENU,
GREY_EDGES_DASHED_MENU,
GREY_EDGES_DASHED_SIDEBAR,
GREY_EDGE_THRESHOLD_MENU,
GREY_EDGE_THRESHOLD_SLIDER_SIDEBAR,
WEIGHTS_SHOW_MOUSE_OVER_CLASS,
WEIGHTS_SHOW_ALWAYS_CLASS,
WEIGHTS_HIDE_CLASS,
SHOW_WEIGHTS_MOUSEOVER,
SHOW_ALL_WEIGHTS,
HIDE_ALL_WEIGHTS,
COLOR_EDGES,
LINK_DIST_SLIDER_SIDEBAR,
LINK_DIST_MENU,
CHARGE_SLIDER_SIDEBAR,
CHARGE_MENU,
CHARGE_DEFAULT_VALUE,
LINK_DIST_DEFAULT_VALUE,
LOCK_SLIDERS_CLASS,
RESET_SLIDERS_CLASS,
UNDO_SLIDERS_RESET_CLASS,
FORCE_GRAPH_BUTTON,
FORCE_GRAPH_MENU,
GRID_LAYOUT_BUTTON,
GRID_LAYOUT_MENU,
NODE_COLORING_TOGGLE_CLASS,
AVG_REPLICATE_VALS_TOP_MENU,
AVG_REPLICATE_VALS_TOP_SIDEBAR,
AVG_REPLICATE_VALS_BOTTOM_MENU,
AVG_REPLICATE_VALS_BOTTOM_SIDEBAR,
LOG_FOLD_CHANGE_MAX_VALUE_SIDEBAR_BUTTON,
LOG_FOLD_CHANGE_MAX_VALUE_MENU,
LOG_FOLD_CHANGE_MAX_VALUE_SIDEBAR_INPUT,
TOP_DATASET_SELECTION_SIDEBAR,
BOTTOM_DATASET_SELECTION_SIDEBAR,
TOP_DATASET_SELECTION_MENU,
BOTTOM_DATASET_SELECTION_MENU,
ZOOM_DISPLAY_MAXIMUM_SELECTOR,
ZOOM_DISPLAY_MAXIMUM_VALUE,
ZOOM_DISPLAY_MINIMUM_SELECTOR,
ZOOM_DISPLAY_MINIMUM_VALUE,
EXPORT_TO_PNG,
EXPORT_TO_SVG,
EXPORT_TO_PDF,
SPECIES_DISPLAY,
SPECIES_BUTTON_CRESS,
SPECIES_BUTTON_FLY,
SPECIES_BUTTON_HUMAN,
SPECIES_BUTTON_MOUSE,
SPECIES_BUTTON_NEMATODE,
SPECIES_BUTTON_YEAST,
VIEWPORT_FIT,
VIEWPORT_INIT,
VIEWPORT_OPTION_CLASS,
VIEWPORT_OPTION_CLASS_SIDEBAR
} from "./constants";
import { setupLoadAndImportHandlers } from "./setup-load-and-import-handlers";
export const setupHandlers = grnState => {
setupLoadAndImportHandlers(grnState);
var valueValidator = (min, max, value) => {
return Math.min(max, Math.max(min, value));
};
const determineFileType = filename => {
if (filename.includes(".xlsx")) {
return ".xlsx";
} else if (filename.includes(".sif")) {
return ".sif";
} else if (filename.includes(".graphml")) {
return ".graphml";
}
};
// thank you for the help https://github.com/nytimes/svg-crowbar/blob/gh-pages/svg-crowbar-2.js
const setInlineStyles = (svg) => {
var emptySvg = window.document.createElementNS("http://www.w3.org/2000/svg", "svg");
window.document.body.appendChild(emptySvg);
var emptySvgDeclarationComputed = getComputedStyle(emptySvg);
const traverse = (node) => {
const tree = [];
const visit = (currentNode) => {
if (currentNode && currentNode.hasChildNodes()) {
let child = currentNode.firstChild;
while (child) {
if (child.nodeType === 1 && child.nodeName !== "SCRIPT") {
tree.push(child);
visit(child);
}
child = child.nextSibling;
}
}
};
visit(node);
return tree;
};
const explicitlySetStyle = element => {
const cssStyleDeclarationComputed = window.getComputedStyle(element);
const computedStyleObj = {};
for (let i = 0; i < cssStyleDeclarationComputed.length; i++) {
const key = cssStyleDeclarationComputed[i];
const value = cssStyleDeclarationComputed.getPropertyValue(key);
if (value !== emptySvgDeclarationComputed.getPropertyValue(key)) {
// Don't set computed style of width and height. Makes SVG elmements disappear.
if ((key !== "height") && (key !== "width")) {
computedStyleObj[key] = value;
}
}
}
if (element.classList.contains("weight")) {
computedStyleObj["visibility"] = "hidden";
}
if (computedStyleObj) {
Object.assign(element.style, computedStyleObj);
}
};
// hardcode computed css styles inside svg
var allElements = traverse(svg);
allElements.forEach(explicitlySetStyle);
};
const sourceAttributeSetter = (svg) => {
svg.setAttribute("version", "1.1");
svg.removeAttribute("xmlns");
svg.removeAttribute("xlink");
if (!svg.hasAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns")) {
svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.w3.org/2000/svg");
}
if (!svg.hasAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink")) {
svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink");
}
};
const exportSVG = (svgElement, name) => {
let source = svgElement.cloneNode(true);
sourceAttributeSetter(source);
setInlineStyles(source);
var doctype = "<?xml version=\"1.0\" standalone=\"no\"?><!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">"; // eslint-disable-line
var sourceString = (new XMLSerializer()).serializeToString(source);
const finalSvgString = [doctype + sourceString];
var svgUrl = window.URL.createObjectURL(new Blob(finalSvgString, { "type" : "image\/svg+xml" }));
$("#exportAsSvg").attr("href", svgUrl);
$("#exportAsSvg").attr("download", name);
};
const exportPDF = (svgElement, name) => {
if (svgElement) {
svgElement = svgElement.replace(/\r?\n|\r/g, "").trim();
}
let canvas = document.createElement("canvas");
canvg(canvas, svgElement);
const imgData = canvas.toDataURL("image/png");
const pdf = new jsPDF("l", "mm", "letter");
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
const svgWidth = canvas.width;
const svgHeight = canvas.height;
const aspectRatio = svgWidth / svgHeight;
let scaledWidth = pdfWidth;
// ratio = width/height --> height = width/ratio
let scaledHeight = scaledWidth / aspectRatio;
if (scaledHeight > pdfHeight) {
scaledHeight = pdfHeight;
scaledWidth = pdfHeight * aspectRatio;
}
pdf.addImage(
imgData,
"PNG",
(pdfWidth - scaledWidth) / 2,
(pdfHeight - scaledHeight) / 2,
scaledWidth,
scaledHeight
);
pdf.save(name);
};
// Grid and Force Graph Layout
$(`${FORCE_GRAPH_MENU}, ${FORCE_GRAPH_BUTTON}`).click(() => {
updaters.setNodesToForceGraph();
grnState.graphLayout = FORCE_GRAPH;
updateApp(grnState);
});
$(`${GRID_LAYOUT_MENU}, ${GRID_LAYOUT_BUTTON}`).click(() => {
updaters.setNodesToGrid();
grnState.graphLayout = GRID_LAYOUT;
updateApp(grnState);
});
// Image Export
$(EXPORT_TO_PNG).click(() => {
var svgContainer = document.getElementById("exportContainer");
var editedName = grnState.name.replace(determineFileType(grnState.name), "") + ".png";
saveSvgAsPng(svgContainer, editedName, { backgroundColor: "white"});
});
$(EXPORT_TO_SVG).click(() => {
var svgContainer = document.getElementById("exportContainer");
var editedName = grnState.name.replace(determineFileType(grnState.name), "") + ".svg";
exportSVG(svgContainer, editedName );
});
$(EXPORT_TO_PDF).click(() => {
var svgContainer = document.getElementById("exportContainer").innerHTML;
var editedName = grnState.name.replace(determineFileType(grnState.name), "") + ".pdf";
exportPDF(svgContainer, editedName);
});
// Node Coloring
const updateTopDatasetSelection = (selection) => {
grnState.nodeColoring.topDataset = selection;
if (grnState.nodeColoring.bottomDataSameAsTop) {
grnState.nodeColoring.bottomDataset = selection;
}
updateApp(grnState);
};
const updateBottomDatasetSelection = (selection) => {
grnState.nodeColoring.bottomDataset = selection;
if (selection === "Same as Top Dataset") {
grnState.nodeColoring.bottomDataset = grnState.nodeColoring.topDataset;
grnState.nodeColoring.bottomDataSameAsTop = true;
} else {
grnState.nodeColoring.bottomDataSameAsTop = false;
grnState.nodeColoring.bottomDataset = selection;
}
updateApp(grnState);
};
$(NODE_COLORING_TOGGLE_CLASS).click(() => {
grnState.nodeColoring.nodeColoringEnabled = !grnState.nodeColoring.nodeColoringEnabled;
updateApp(grnState);
});
$(TOP_DATASET_SELECTION_SIDEBAR).change(() => {
const selection = $(TOP_DATASET_SELECTION_SIDEBAR).find(":selected").attr("value");
updateTopDatasetSelection(selection);
});
$(TOP_DATASET_SELECTION_MENU).click((event) => {
const selection = event.target.dataset.expression;
updateTopDatasetSelection(selection);
});
$(AVG_REPLICATE_VALS_TOP_SIDEBAR).change(() => {
grnState.nodeColoring.averageTopDataset = !grnState.nodeColoring.averageTopDataset;
updateApp(grnState);
});
$(AVG_REPLICATE_VALS_TOP_MENU).click(() => {
grnState.nodeColoring.averageTopDataset = !grnState.nodeColoring.averageTopDataset;
updateApp(grnState);
});
$(BOTTOM_DATASET_SELECTION_SIDEBAR).change(() => {
const selection = $(BOTTOM_DATASET_SELECTION_SIDEBAR).find(":selected").attr("value");
updateBottomDatasetSelection(selection);
});
$(BOTTOM_DATASET_SELECTION_MENU).click((event) => {
const selection = event.target.dataset.expression;
updateBottomDatasetSelection(selection);
});
$(AVG_REPLICATE_VALS_BOTTOM_SIDEBAR).change(() => {
grnState.nodeColoring.averageBottomDataset = !grnState.nodeColoring.averageBottomDataset;
updateApp(grnState);
});
$(AVG_REPLICATE_VALS_BOTTOM_MENU).click(() => {
grnState.nodeColoring.averageBottomDataset = !grnState.nodeColoring.averageBottomDataset;
updateApp(grnState);
});
var logFoldChangeMaxValueValidator = value => {
return valueValidator(0.01, 100, value);
};
$(LOG_FOLD_CHANGE_MAX_VALUE_SIDEBAR_BUTTON).click(() => {
var value = logFoldChangeMaxValueValidator($(LOG_FOLD_CHANGE_MAX_VALUE_SIDEBAR_INPUT).val());
grnState.nodeColoring.logFoldChangeMaxValue = value;
updateApp(grnState);
});
$(LOG_FOLD_CHANGE_MAX_VALUE_MENU).change(() => {
var value = logFoldChangeMaxValueValidator($(LOG_FOLD_CHANGE_MAX_VALUE_MENU).val());
grnState.nodeColoring.logFoldChangeMaxValue = value;
updateApp(grnState);
});
// Sliders Code
var linkDistValidator = value => {
return valueValidator(1, 1000, value);
};
var chargeValidator = value => {
return valueValidator(-2000, 0, value);
};
$(LINK_DIST_SLIDER_SIDEBAR).change(() => {
var value = linkDistValidator($(LINK_DIST_SLIDER_SIDEBAR).val());
grnState.linkDistanceSlider.currentVal = value;
updateApp(grnState);
});
$(LINK_DIST_MENU).change(() => {
var value = linkDistValidator($(LINK_DIST_MENU).val());
grnState.linkDistanceSlider.currentVal = value;
updateApp(grnState);
});
$(CHARGE_SLIDER_SIDEBAR).change(() => {
var value = chargeValidator($(CHARGE_SLIDER_SIDEBAR).val());
grnState.chargeSlider.currentVal = value;
updateApp(grnState);
});
$(CHARGE_MENU).change(() => {
var value = chargeValidator($(CHARGE_MENU).val());
grnState.chargeSlider.currentVal = value;
updateApp(grnState);
});
$(LOCK_SLIDERS_CLASS).click(() => {
grnState.slidersLocked = !grnState.slidersLocked;
if (grnState.slidersLocked) {
grnState.showUndoReset = false;
}
updateApp(grnState);
});
$(VIEWPORT_OPTION_CLASS_SIDEBAR).click(function () {
grnState.viewportSize = $(this).val();
updateApp(grnState);
});
$(VIEWPORT_OPTION_CLASS).click(function () {
grnState.viewportSize = $(this).attr("value");
updateApp(grnState);
});
$(RESET_SLIDERS_CLASS).click(() => {
grnState.chargeSlider.backup = grnState.chargeSlider.currentVal;
grnState.linkDistanceSlider.backup = grnState.linkDistanceSlider.currentVal;
grnState.chargeSlider.currentVal = CHARGE_DEFAULT_VALUE;
grnState.linkDistanceSlider.currentVal = LINK_DIST_DEFAULT_VALUE;
grnState.showUndoReset = true;
updateApp(grnState);
});
$(UNDO_SLIDERS_RESET_CLASS).click(() => {
grnState.chargeSlider.currentVal = grnState.chargeSlider.backup;
grnState.linkDistanceSlider.currentVal = grnState.linkDistanceSlider.backup;
grnState.showUndoReset = false;
updateApp(grnState);
});
// Weights Visualization
$(WEIGHTS_SHOW_ALWAYS_CLASS).click(() => {
grnState.edgeWeightDisplayOption = SHOW_ALL_WEIGHTS;
updateApp(grnState);
});
$(WEIGHTS_HIDE_CLASS).click(() => {
grnState.edgeWeightDisplayOption = HIDE_ALL_WEIGHTS;
updateApp(grnState);
});
// Normalization Options
$(SET_NORMALIZATION_SIDEBAR).click(() => {
grnState.normalizationMax = $(SET_NORMALIZATION_SIDEBAR_VALUE).val();
updateApp(grnState);
});
$(SET_NORMALIZATION_MENU).change(() => {
grnState.normalizationMax = $(SET_NORMALIZATION_MENU).val();
updateApp(grnState);
});
$(RESET_NORMALIZATION_SIDEBAR).click(() => {
grnState.normalizationMax = grnState.resetNormalizationMax;
updateApp(grnState);
});
$(RESET_NORMALIZATION_MENU).click(() => {
grnState.normalizationMax = grnState.resetNormalizationMax;
updateApp(grnState);
});
// Grey Edges
$(GREY_EDGES_DASHED_SIDEBAR).change(() => {
grnState.dashedLine = $(GREY_EDGES_DASHED_SIDEBAR).prop("checked");
updateApp(grnState);
});
$(GREY_EDGES_DASHED_MENU).click(() => {
grnState.dashedLine = !$(GREY_EDGES_DASHED_MENU).prop("checked");
updateApp(grnState);
});
$(GREY_EDGE_THRESHOLD_MENU).change(() => {
grnState.grayEdgeThreshold = Math.round($(GREY_EDGE_THRESHOLD_MENU).val());
updateApp(grnState);
});
$(GREY_EDGE_THRESHOLD_SLIDER_SIDEBAR).change(() => {
grnState.grayEdgeThreshold = Math.round($(GREY_EDGE_THRESHOLD_SLIDER_SIDEBAR).val() * 100);
updateApp(grnState);
});
$(WEIGHTS_SHOW_MOUSE_OVER_CLASS).click(() => {
grnState.edgeWeightDisplayOption = SHOW_WEIGHTS_MOUSEOVER;
updateApp(grnState);
});
$(COLOR_EDGES).click(() => {
grnState.colorOptimal = !grnState.colorOptimal;
updateApp(grnState);
});
// Species Selection
$(SPECIES_DISPLAY).change(() => {
var selection = $(SPECIES_DISPLAY).find(":selected").attr("value");
grnState.genePageData.identified = false;
identifySpeciesMenu(selection);
updateApp(grnState);
});
$(SPECIES_BUTTON_YEAST).click(function () {
var selection = $(SPECIES_BUTTON_YEAST).attr("id");
grnState.genePageData.identified = false;
identifySpeciesMenu(selection);
updateApp(grnState);
});
$(SPECIES_BUTTON_MOUSE).click(function () {
var selection = $(SPECIES_BUTTON_MOUSE).attr("id");
grnState.genePageData.identified = false;
identifySpeciesMenu(selection);
updateApp(grnState);
});
$(SPECIES_BUTTON_HUMAN).click(function () {
var selection = $(SPECIES_BUTTON_HUMAN).attr("id");
grnState.genePageData.identified = false;
identifySpeciesMenu(selection);
updateApp(grnState);
});
$(SPECIES_BUTTON_NEMATODE).click(function () {
var selection = $(SPECIES_BUTTON_NEMATODE).attr("id");
grnState.genePageData.identified = false;
identifySpeciesMenu(selection);
updateApp(grnState);
});
$(SPECIES_BUTTON_CRESS).click(function () {
var selection = $(SPECIES_BUTTON_CRESS).attr("id");
grnState.genePageData.identified = false;
identifySpeciesMenu(selection);
updateApp(grnState);
});
$(SPECIES_BUTTON_FLY).click(function () {
var selection = $(SPECIES_BUTTON_FLY).attr("id");
grnState.genePageData.identified = false;
identifySpeciesMenu(selection);
updateApp(grnState);
});
// Allow text-selection for input elements embedded within menu items.
//
// Partial thank you:
// https://stackoverflow.com/questions/6848140/how-do-i-prevent-drag-on-a-child-but-allow-drag-on-the-parent
//
// We use function syntax so that internal `this` can be used.
$(".dropdown input.keepopen").parent().attr({
draggable: false
});
// Prevent Bootstrap dropdown from closing on clicks in menu input boxes
// https://stackoverflow.com/a/27759926
$(".dropdown").on({
"click": function (event) {
if ($(event.target).hasClass("keepopen")) {
$(this).data("closable", $(event.target).closest(".dropdown-toggle").length !== 0);
}
},
"mouseup": function (event) {
if ($(event.target).find(".keepopen").length > 0) {
$(this).data("closable", $(event.target).closest(".dropdown-toggle").length !== 0);
}
},
"hide.bs.dropdown": function () {
var hide = $(this).data("closable");
$(this).data("closable", true);
return hide;
}
});
$(ZOOM_DISPLAY_MAXIMUM_SELECTOR).text(ZOOM_DISPLAY_MAXIMUM_VALUE);
$(ZOOM_DISPLAY_MINIMUM_SELECTOR).text(ZOOM_DISPLAY_MINIMUM_VALUE);
// Viewport handling.
const setupContainer = function () {
const MEDIUM_PAGE_WIDTH = 1500;
const LARGE_PAGE_WIDTH = 2200;
const initializeViewportSize = (width) => {
if (width < MEDIUM_PAGE_WIDTH) {
grnState.viewportSize = "containerS";
} else if (width > MEDIUM_PAGE_WIDTH && width < LARGE_PAGE_WIDTH) {
grnState.viewportSize = "containerM";
} else {
grnState.viewportSize = "containerL";
}
};
// For viewport-fit, we create a transient `dimensions` property in `grnState`
// which `updateApp` will use to determine the latest size.
//
// For any other size, we blank out the transient `dimensions`.
if (window === window.top) {
initializeViewportSize($(window).width());
$(window).on("resize", () => {
if (grnState.viewportSize === VIEWPORT_FIT) {
grnState.dimensions = {
width: $(window).width(),
height: $(window).height(),
top: 0
};
updateApp(grnState);
} else {
delete grnState.dimensions;
}
});
} else {
window.addEventListener("message", event => {
if (event.origin.indexOf(HOST_SITE) !== 0) {
return;
}
// Also filter out all but dimensions messages.
const { data } = event;
if (typeof(data) !== "object" || (
data.width === undefined ||
data.height === undefined ||
data.top === undefined
)) {
return;
}
// Live changes only matter if we are doing Fit to Window or initializing.
if ([VIEWPORT_FIT, VIEWPORT_INIT].includes(grnState.viewportSize)) {
if (grnState.viewportSize === VIEWPORT_INIT) {
initializeViewportSize(data.width);
}
if (grnState.viewportSize === VIEWPORT_FIT) {
grnState.dimensions = data;
}
updateApp(grnState);
} else {
delete grnState.dimensions;
}
});
}
$("#restrict-graph-to-viewport").on("click", function () {
if ($(".viewport").prop("checked")) {
$("#restrict-graph-to-viewport span").removeClass("glyphicon-ok");
$(".viewport").prop("checked", false);
$(".viewport").trigger("change");
} else {
$("#restrict-graph-to-viewport span").addClass("glyphicon-ok");
$(".viewport").prop("checked", true).trigger("change");
}
});
$("#expressionDB").on("click", function () {
if (!$("#expressionDB span").hasClass("glyphicon-ok")) {
$("#expressionDB span").addClass("glyphicon-ok");
$("#expressionDB").trigger("change");
} else {
$("#expressionDB span").removeClass("glyphicon-ok");
$("#expressionDB").trigger("change");
}
});
};
setupContainer();
};