diff --git a/src/datasets_synchronization/views.py b/src/datasets_synchronization/views.py index 3e7097f7..ccfe9ab0 100644 --- a/src/datasets_synchronization/views.py +++ b/src/datasets_synchronization/views.py @@ -14,6 +14,7 @@ from .models import CGDSStudy, CGDSDatasetSynchronizationState, CGDSStudySynchronizationState, CGDSDataset from rest_framework import generics, permissions, filters from django_filters.rest_framework import DjangoFilterBackend +from django_filters.rest_framework import DjangoFilterBackend from user_files.models_choices import FileType from .serializers import CGDSStudySerializer from django.shortcuts import render, get_object_or_404 diff --git a/src/frontend/static/frontend/package-lock.json b/src/frontend/static/frontend/package-lock.json index b6e3a75c..bbca754f 100644 --- a/src/frontend/static/frontend/package-lock.json +++ b/src/frontend/static/frontend/package-lock.json @@ -16,7 +16,7 @@ "@visx/tooltip": "^3.3.0", "apexcharts": "^3.52.0", "axios": "^1.8.4", - "cytoscape": "^3.30.2", + "cytoscape": "^3.33.1", "d3": "^7.9.0", "dayjs": "^1.11.13", "fomantic-ui-css": "^2.9.4", @@ -10122,9 +10122,10 @@ "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==" }, "node_modules/cytoscape": { - "version": "3.30.2", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.2.tgz", - "integrity": "sha512-oICxQsjW8uSaRmn4UK/jkczKOqTrVqt5/1WL0POiJUT2EKNc9STM4hYFHv917yu55aTBMFNRzymlJhVAiWPCxw==", + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "license": "MIT", "engines": { "node": ">=0.10" } @@ -23101,7 +23102,6 @@ "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-3.3.0.tgz", "integrity": "sha512-3PT9dW7IbIfN7JWGr4YxxFQnbN5MRaB36qIKF/eF0iC9l0/MuGSlMlgRgI7Uu8vYuGxX6AjLwsBBRYTPG7NFSA==", "license": "MIT", - "peer": true, "dependencies": { "@plotly/d3": "3.8.2", "@plotly/d3-sankey": "0.7.2", diff --git a/src/frontend/static/frontend/package.json b/src/frontend/static/frontend/package.json index fd73478c..76dd01fa 100644 --- a/src/frontend/static/frontend/package.json +++ b/src/frontend/static/frontend/package.json @@ -59,7 +59,7 @@ "@visx/tooltip": "^3.3.0", "apexcharts": "^3.52.0", "axios": "^1.8.4", - "cytoscape": "^3.30.2", + "cytoscape": "^3.33.1", "d3": "^7.9.0", "dayjs": "^1.11.13", "fomantic-ui-css": "^2.9.4", diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/CurrentMoleculeDetails.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/CurrentMoleculeDetails.tsx index 4f463eeb..1f173352 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/CurrentMoleculeDetails.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/CurrentMoleculeDetails.tsx @@ -11,6 +11,7 @@ import { GeneAssociationsNetworkPanel } from './genes/GeneAssociationsNetworkPan import { MiRNADrugsPanel } from '../../../pipeline/experiment-result/gene-gem-details/MiRNADrugsPanel' import { MiRNADiseasesPanel } from '../../../pipeline/experiment-result/gene-gem-details/MiRNADiseasesPanel' import { ActionableCancerGenesPanel } from './genes/ActionableCancerGenesPanel' +import { GeneExpressionRegulationNetworkPanel } from './genes/GeneAssociationsNetwork' // const MENU_DEFAULT: ActiveBiomarkerMoleculeItemMenu = ActiveBiomarkerMoleculeItemMenu.DETAILS // TODO: use this const MENU_DEFAULT: ActiveBiomarkerMoleculeItemMenu = ActiveBiomarkerMoleculeItemMenu.DETAILS @@ -52,6 +53,8 @@ export const CurrentMoleculeDetails = (props: CurrentMoleculeDetailsProps) => { return case ActiveBiomarkerMoleculeItemMenu.GENE_ONTOLOGY: return + case ActiveBiomarkerMoleculeItemMenu.GENE_REGULATION_ASSOCIATIONS: + return case ActiveBiomarkerMoleculeItemMenu.DISEASES: return case ActiveBiomarkerMoleculeItemMenu.DRUGS: diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/MoleculesDetailsMenu.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/MoleculesDetailsMenu.tsx index 5a98008b..ec0b914c 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/MoleculesDetailsMenu.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/MoleculesDetailsMenu.tsx @@ -63,6 +63,13 @@ export const MoleculesDetailsMenu = (props: MoleculesDetailsMenuProps) => { popupInfo: 'Gene Ontology (GO) is a powerful tool for understanding the biological processes, molecular functions, and cellular components associated with a gene', isVisible: isGene }, + { + name: 'Gene regulation associations', + onClick: () => props.setActiveItem(ActiveBiomarkerMoleculeItemMenu.GENE_REGULATION_ASSOCIATIONS), + isActive: props.activeItem === ActiveBiomarkerMoleculeItemMenu.GENE_REGULATION_ASSOCIATIONS, + popupInfo: 'Gene regulation associations provide insights into the regulatory relationships between genes, helping to unravel the complex mechanisms that control gene expression and cellular function', + isVisible: isGene + }, // TODO: implement // { // name: 'Actionable/Cancer genes', diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/gene-ontology/GeneOntologyCytoscapeChart.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/gene-ontology/GeneOntologyCytoscapeChart.tsx index 9b277bba..af7df28e 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/gene-ontology/GeneOntologyCytoscapeChart.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/gene-ontology/GeneOntologyCytoscapeChart.tsx @@ -78,7 +78,7 @@ export const GeneOntologyCytoscapeChart = (props: GeneOntologyCytoscapeChartProp style: { 'curve-style': 'bezier', 'target-arrow-shape': 'triangle', - 'line-color': function (edge) { + 'line-color': function (edge): any { // Sets the color of the edge depending on the relation_type attribute const relationType: OntologyRelationTermToTermFilter = edge.data('relation_type') diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetwork.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetwork.tsx new file mode 100644 index 00000000..ad7f0e0d --- /dev/null +++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetwork.tsx @@ -0,0 +1,341 @@ +import React, { useEffect, useMemo, useRef } from 'react' +import cytoscape, { Core, ElementDefinition } from 'cytoscape' + +type NodeKind = 'mRNA' | 'miRNA' | 'CNA' | 'Methylation' | 'Drug' + +type Props = { + height?: number | string; + width?: number | string; +} + +const NODE_COLORS: Record = { + mRNA: '#4f46e5', + miRNA: '#db2777', + CNA: '#f59e0b', + Methylation: '#10b981', + Drug: '#64748b', +} + +const getEdgeColor = (correlation: number) => { + if (correlation <= -0.5) { return '#dc2626' } + + if (correlation >= 0.5) { return '#2563eb' } + + return '#cbd5e1' +} + +const getEdgeOpacity = (correlation: number) => { + const abs = Math.abs(correlation) + + if (abs >= 0.8) { return 0.95 } + if (abs >= 0.6) { return 0.8 } + if (abs >= 0.5) { return 0.65 } + + return 0.22 +} + +const getEdgeWidth = (correlation: number) => { + const abs = Math.abs(correlation) + + if (abs >= 0.9) { return 6 } + if (abs >= 0.8) { return 5 } + if (abs >= 0.7) { return 4 } + if (abs >= 0.5) { return 3 } + + return 1.5 +} + +export const GeneExpressionRegulationNetworkPanel = ({ + height = 650, + width = '100%', +}: Props) => { + const containerRef = useRef(null) + const cyRef = useRef(null) + + const elements = useMemo(() => { + const nodes: ElementDefinition[] = [ + { data: { id: 'mrna_pcna', label: 'PCNA', type: 'mRNA', size: 58 } }, + { data: { id: 'mrna_fen1', label: 'FEN1', type: 'mRNA', size: 54 } }, + { data: { id: 'mrna_rad51', label: 'RAD51', type: 'mRNA', size: 36 } }, + { data: { id: 'mrna_pold1', label: 'POLD1', type: 'mRNA', size: 32 } }, + { data: { id: 'mrna_lig1', label: 'LIG1', type: 'mRNA', size: 30 } }, + { data: { id: 'mir_21', label: 'miR-21', type: 'miRNA', size: 28 } }, + { data: { id: 'mir_34a', label: 'miR-34a', type: 'miRNA', size: 28 } }, + { data: { id: 'mir_155', label: 'miR-155', type: 'miRNA', size: 28 } }, + { data: { id: 'mir_200c', label: 'miR-200c', type: 'miRNA', size: 28 } }, + { data: { id: 'cna_8q24', label: 'CNA 8q24', type: 'CNA', size: 34 } }, + { data: { id: 'cna_17p_loss', label: '17p loss', type: 'CNA', size: 34 } }, + { data: { id: 'cna_1q_gain', label: '1q gain', type: 'CNA', size: 34 } }, + { data: { id: 'meth_mlh1', label: 'MLH1 meth', type: 'Methylation', size: 32 } }, + { data: { id: 'meth_mgmt', label: 'MGMT meth', type: 'Methylation', size: 32 } }, + { data: { id: 'drug_olaparib', label: 'Olaparib', type: 'Drug', size: 34 } }, + { data: { id: 'drug_cisplatin', label: 'Cisplatin', type: 'Drug', size: 34 } }, + { data: { id: 'drug_temozolomide', label: 'Temozolomide', type: 'Drug', size: 34 } }, + { data: { id: 'mrna_apex2', label: 'APEX2', type: 'mRNA', size: 32 } }, + ] + + const rawEdges = [ + ['mrna_pcna', 'mrna_fen1', 0.92], + ['mrna_pcna', 'mrna_rad51', 0.87], + ['mrna_pcna', 'mrna_pold1', 0.83], + ['mrna_pcna', 'mrna_lig1', 0.79], + ['mrna_pcna', 'mrna_apex2', 0.72], + ['mrna_fen1', 'mrna_rad51', 0.76], + ['mrna_fen1', 'mrna_pold1', 0.81], + ['mrna_fen1', 'mrna_apex2', 0.69], + ['mrna_rad51', 'mrna_apex2', 0.64], + ['mrna_pold1', 'mrna_lig1', 0.71], + ['mrna_lig1', 'mrna_apex2', 0.58], + + ['mir_21', 'mrna_pcna', -0.74], + ['mir_21', 'mrna_fen1', -0.68], + ['mir_21', 'mrna_rad51', -0.61], + ['mir_34a', 'mrna_pcna', -0.71], + ['mir_34a', 'mrna_pold1', -0.66], + ['mir_155', 'mrna_rad51', -0.78], + ['mir_155', 'mrna_apex2', -0.57], + ['mir_200c', 'mrna_lig1', -0.63], + ['mir_200c', 'mrna_fen1', -0.55], + + ['cna_8q24', 'mrna_pcna', 0.67], + ['cna_8q24', 'mrna_fen1', 0.54], + ['cna_17p_loss', 'mrna_rad51', -0.58], + ['cna_17p_loss', 'mrna_apex2', -0.52], + ['cna_1q_gain', 'mrna_pold1', 0.62], + ['cna_1q_gain', 'mrna_lig1', 0.57], + ['meth_mlh1', 'mrna_pcna', -0.53], + ['meth_mlh1', 'mrna_apex2', -0.64], + ['meth_mgmt', 'mrna_rad51', -0.59], + ['meth_mgmt', 'mrna_fen1', -0.51], + + ['drug_olaparib', 'mrna_rad51', -0.69], + ['drug_olaparib', 'mrna_fen1', -0.56], + ['drug_cisplatin', 'mrna_pcna', -0.52], + ['drug_cisplatin', 'mrna_lig1', -0.55], + ['drug_temozolomide', 'meth_mgmt', 0.73], + ['drug_temozolomide', 'mrna_apex2', -0.51], + + ['mir_21', 'cna_8q24', 0.22], + ['mir_34a', 'meth_mlh1', -0.18], + ['drug_olaparib', 'drug_cisplatin', 0.31], + ['mrna_pcna', 'drug_temozolomide', -0.27], + ['cna_1q_gain', 'mrna_apex2', 0.33], + ] as const + + const edges: ElementDefinition[] = rawEdges.map(([source, target, correlation], index) => ({ + data: { + id: `e_${index + 1}`, + source, + target, + correlation, + edgeColor: getEdgeColor(correlation), + edgeOpacity: getEdgeOpacity(correlation), + edgeWidth: getEdgeWidth(correlation), + }, + classes: Math.abs(correlation) < 0.5 ? 'weak-edge' : '', + })) + + return [...nodes, ...edges] + }, []) + + useEffect(() => { + if (!containerRef.current) { return } + + const cy = cytoscape({ + container: containerRef.current, + elements, + wheelSensitivity: 0.18, + minZoom: 0.4, + maxZoom: 2, + style: [ + { + selector: 'node', + style: { + label: 'data(label)', + width: 'data(size)', + height: 'data(size)', + shape: 'ellipse', + 'background-color': '#64748b', + color: '#ffffff', + 'font-size': 12, + 'font-weight': 600, + 'text-valign': 'center', + 'text-halign': 'center', + 'text-wrap': 'wrap', + 'text-max-width': 90 as any, + 'text-outline-color': '#475569', + 'text-outline-width': 3, + 'border-width': 2, + 'border-color': '#ffffff', + }, + }, + { + selector: 'node[type = "mRNA"]', + style: { + 'background-color': NODE_COLORS.mRNA, + 'text-outline-color': NODE_COLORS.mRNA, + }, + }, + { + selector: 'node[type = "miRNA"]', + style: { + 'background-color': NODE_COLORS.miRNA, + 'text-outline-color': NODE_COLORS.miRNA, + }, + }, + { + selector: 'node[type = "CNA"]', + style: { + 'background-color': NODE_COLORS.CNA, + 'text-outline-color': NODE_COLORS.CNA, + }, + }, + { + selector: 'node[type = "Methylation"]', + style: { + 'background-color': NODE_COLORS.Methylation, + 'text-outline-color': NODE_COLORS.Methylation, + }, + }, + { + selector: 'node[type = "Drug"]', + style: { + 'background-color': NODE_COLORS.Drug, + 'text-outline-color': NODE_COLORS.Drug, + }, + }, + { + selector: 'edge', + style: { + width: 'data(edgeWidth)', + 'line-color': 'data(edgeColor)', + opacity: 'data(edgeOpacity)' as any, + 'curve-style': 'bezier', + }, + }, + { + selector: 'edge.weak-edge', + style: { + 'line-style': 'dashed', + }, + }, + { + selector: ':selected', + style: { + 'border-color': '#f8fafc', + 'border-width': 5, + 'line-color': '#0f172a', + 'target-arrow-color': '#0f172a', + 'source-arrow-color': '#0f172a', + }, + }, + { + selector: '.faded', + style: { + opacity: 0.12, + }, + }, + ], + layout: { + name: 'cose', + animate: true, + randomize: true, + fit: true, + padding: 40, + gravity: 1, + nodeRepulsion: 9000, + idealEdgeLength: 90, + componentSpacing: 80, + }, + }) + + cyRef.current = cy + + cy.on('tap', 'node', (evt) => { + const node = evt.target + const neighborhood = node.closedNeighborhood() + + cy.elements().addClass('faded') + neighborhood.removeClass('faded') + }) + + cy.on('tap', (evt) => { + if (evt.target === cy) { + cy.elements().removeClass('faded') + cy.elements().unselect() + } + }) + + return () => { + cy.destroy() + cyRef.current = null + } + }, [elements]) + + return ( +
+
+
+ + + + + + + +
+
+ +
+
+ ) +} + +const LegendDot = ({ color, label }: { color: string; label: string }) => ( + + + {label} + +) + +const LegendLine = ({ color, label }: { color: string; label: string }) => ( + + + {label} + +) diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetworkPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetworkPanel.tsx index a7d5db85..4a4e00ab 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetworkPanel.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetworkPanel.tsx @@ -1,6 +1,11 @@ -import React, { useEffect, useRef } from 'react' +import React, { useEffect, useRef, useState } from 'react' import ky from 'ky' -import cytoscape from 'cytoscape' +import cytoscape, { + Core, + ElementDefinition, + EventObjectNode, + NodeSingular +} from 'cytoscape' import { BiomarkerMolecule } from '../../../types' import { Form, Grid, Input } from 'semantic-ui-react' import { alertGeneralError } from '../../../../../utils/util_functions' @@ -11,240 +16,314 @@ import '../../../../../css/cytoscape.css' // Defined in biomarkers.html declare const urlGeneAssociationsNetwork: string -/** Colors for the String standard: https://string-db.org/cgi/help ("Network" section). */ const COLORS_BY_STRING_RELATION = { fusion: ['Fusion', '#d90429'], coOccurrence: ['Co-occurrence', '#3a86ff'], experimental: ['Experimental', '#7209b7'], textMining: ['Text mining', '#d5ae5d'], database: ['Database', '#4cc9f0'], - coExpression: ['Co-expression', '#10002b'] // In the FAQ says 'white', but we have a white background, so it's not visible -} + coExpression: ['Co-expression', '#10002b'] +} as const -/** - * Renders the legends for the Cytoscape instance. - * @returns Component. - */ const CytoscapeLegends = () => (
Relations
    {Object.entries(COLORS_BY_STRING_RELATION).map(([_, [relationDescription, color]]) => ( -
  • {relationDescription}
  • +
  • + + {relationDescription} +
  • ))}
) -/** GeneAssociationsNetworkPanel props. */ interface GeneAssociationsNetworkPanelProps { - /** Selected BiomarkerMolecule instance to show the options. */ - selectedGene: BiomarkerMolecule, + selectedGene: BiomarkerMolecule } -export const GeneAssociationsNetworkPanel = (props: GeneAssociationsNetworkPanelProps) => { - const abortControllerTerm = useRef(new AbortController()) - const [minCombinedScore, setMinCombinedScore] = React.useState(950) +type CytoscapeResponseData = + | ElementDefinition[] + | { + nodes?: ElementDefinition[] + edges?: ElementDefinition[] + } + +export const GeneAssociationsNetworkPanel = ({ selectedGene }: GeneAssociationsNetworkPanelProps) => { + const cyContainerRef = useRef(null) + const cyRef = useRef(null) + const abortControllerRef = useRef(null) + const positionsRef = useRef>({}) + const [minCombinedScore, setMinCombinedScore] = useState(950) + + const cytoscapeStyles = [ + { + selector: 'core', + style: { + 'selection-box-color': '#AAD8FF', + 'selection-box-border-color': '#8BB0D0', + 'selection-box-opacity': 0.5 + } as any + }, + { + selector: 'node', + style: { + width: 'mapData(score, 0, 0.006769776522008331, 20, 60)', + height: 'mapData(score, 0, 0.006769776522008331, 20, 60)', + content: 'data(name)', + 'font-size': '12px', + 'text-valign': 'center', + 'text-halign': 'center', + 'background-color': '#555', + 'text-outline-color': '#555', + 'text-outline-width': '2px', + color: '#fff', + 'overlay-padding': '6px', + 'z-index': 10 + } + }, + { + selector: 'node[?attr]', + style: { + shape: 'rectangle', + 'background-color': '#aaa', + 'text-outline-color': '#aaa', + width: '16px', + height: '16px', + 'font-size': '6px', + 'z-index': 1 + } + }, + { + selector: 'node[?query]', + style: { + 'background-clip': 'none', + 'background-fit': 'contain' + } + }, + { + selector: 'node:selected', + style: { + 'border-width': '6px', + 'border-color': '#AAD8FF', + 'border-opacity': 0.5, + 'background-color': '#77828C', + 'text-outline-color': '#77828C' + } + }, + { + selector: 'edge', + style: { + 'curve-style': 'haystack', + 'haystack-radius': 0.5, + opacity: 0.4, + 'line-color': '#bbb', + width: 'mapData(weight, 0, 1, 1, 8)', + 'overlay-padding': '3px' + } + }, + { + selector: 'node.unhighlighted', + style: { + opacity: 0.2 + } + }, + { + selector: 'edge.unhighlighted', + style: { + opacity: 0.05 + } + }, + { + selector: '.highlighted', + style: { + 'z-index': 999999 + } + }, + { + selector: 'node.highlighted', + style: { + 'border-width': '6px', + 'border-color': '#AAD8FF', + 'border-opacity': 0.5, + 'background-color': '#394855', + 'text-outline-color': '#394855' + } + }, + { + selector: 'edge.filtered', + style: { + opacity: 0 + } + }, + { + selector: 'edge[group="fusion"]', + style: { + 'line-color': COLORS_BY_STRING_RELATION.fusion[1] + } + }, + { + selector: 'edge[group="coOccurrence"]', + style: { + 'line-color': COLORS_BY_STRING_RELATION.coOccurrence[1] + } + }, + { + selector: 'edge[group="experimental"]', + style: { + 'line-color': COLORS_BY_STRING_RELATION.experimental[1] + } + }, + { + selector: 'edge[group="textMining"]', + style: { + 'line-color': COLORS_BY_STRING_RELATION.textMining[1] + } + }, + { + selector: 'edge[group="database"]', + style: { + 'line-color': COLORS_BY_STRING_RELATION.database[1] + } + }, + { + selector: 'edge[group="coExpression"]', + style: { + 'line-color': COLORS_BY_STRING_RELATION.coExpression[1] + } + } + ] + + const normalizeElements = (elements: CytoscapeResponseData): ElementDefinition[] => { + if (Array.isArray(elements)) { + return elements + } + + return [...(elements.nodes || []), ...(elements.edges || [])] + } + + const createCy = () => { + if (cyRef.current || !cyContainerRef.current) { return } - /** - * Initializes the Cytoscape instance with the given elements. - * @param elements Cytoscape elements to initialize the instance. - */ - const initCytoscape = (elements: any /* TODO: type */) => { const cy = cytoscape({ - container: document.getElementById('cy'), + container: cyContainerRef.current, minZoom: 0.5, maxZoom: 1.5, - randomize: true, - animate: true, - nodeSpacing: 15, - edgeLengthVal: 45, - style: [ - { - selector: 'core', - style: { - 'selection-box-color': '#AAD8FF', - 'selection-box-border-color': '#8BB0D0', - 'selection-box-opacity': '0.5' - } - }, - { - selector: 'node', - style: { - width: 'mapData(score, 0, 0.006769776522008331, 20, 60)', - height: 'mapData(score, 0, 0.006769776522008331, 20, 60)', - content: 'data(name)', - 'font-size': '12px', - 'text-valign': 'center', - 'text-halign': 'center', - 'background-color': '#555', - 'text-outline-color': '#555', - 'text-outline-width': '2px', - color: '#fff', - 'overlay-padding': '6px', - 'z-index': '10' - } - }, - { - selector: 'node[?attr]', - style: { - shape: 'rectangle', - 'background-color': '#aaa', - 'text-outline-color': '#aaa', - width: '16px', - height: '16px', - 'font-size': '6px', - 'z-index': '1' - } - }, - { - selector: 'node[?query]', - style: { - 'background-clip': 'none', - 'background-fit': 'contain' - } - }, - { - selector: 'node:selected', - style: { - 'border-width': '6px', - 'border-color': '#AAD8FF', - 'border-opacity': '0.5', - 'background-color': '#77828C', - 'text-outline-color': '#77828C' - } - }, - { - selector: 'edge', - style: { - 'curve-style': 'haystack', - 'haystack-radius': '0.5', - opacity: '0.4', - 'line-color': '#bbb', - width: 'mapData(weight, 0, 1, 1, 8)', - 'overlay-padding': '3px' - } - }, - { - selector: 'node.unhighlighted', - style: { - opacity: '0.2' - } - }, - { - selector: 'edge.unhighlighted', - style: { - opacity: '0.05' - } - }, - { - selector: '.highlighted', - style: { - 'z-index': '999999' - } - }, - { - selector: 'node.highlighted', - style: { - 'border-width': '6px', - 'border-color': '#AAD8FF', - 'border-opacity': '0.5', - 'background-color': '#394855', - 'text-outline-color': '#394855' - } - }, - { - selector: 'edge.filtered', - style: { - opacity: '0' - } - }, - // Types of relations - { - selector: 'edge[group="fusion"]', - style: { - 'line-color': COLORS_BY_STRING_RELATION.fusion[1] - } - }, - { - selector: 'edge[group="coOccurrence"]', - style: { - 'line-color': COLORS_BY_STRING_RELATION.coOccurrence[1] - } - }, - { - selector: 'edge[group="experimental"]', - style: { - 'line-color': COLORS_BY_STRING_RELATION.experimental[1] - } - }, - { - selector: 'edge[group="textMining"]', - style: { - 'line-color': COLORS_BY_STRING_RELATION.textMining[1] - } - }, - { - selector: 'edge[group="database"]', - style: { - 'line-color': COLORS_BY_STRING_RELATION.database[1] - } - }, - { - selector: 'edge[group="coExpression"]', - style: { - 'line-color': COLORS_BY_STRING_RELATION.coExpression[1] - } + elements: [], + style: cytoscapeStyles, + layout: { + name: 'preset' + } + }) + + cy.on('dragfreeon', 'node', (evt: EventObjectNode) => { + const node = evt.target + positionsRef.current[node.id()] = node.position() + }) + + cyRef.current = cy + } + + const applyElements = (rawElements: CytoscapeResponseData) => { + const cy = cyRef.current + + if (!cy) { return } + + const incomingElements = normalizeElements(rawElements) + + cy.nodes().forEach((node: NodeSingular) => { + positionsRef.current[node.id()] = node.position() + }) + + const mergedElements = incomingElements.map((element) => { + const elementId = element?.data?.id as string | undefined + const savedPosition = elementId ? positionsRef.current[elementId] : undefined + const isNode = !('source' in (element.data || {})) && !('target' in (element.data || {})) + + if (isNode && savedPosition) { + return { + ...element, + position: savedPosition } - ], - elements + } + + return element }) - const layout = cy.elements().layout({ - name: 'random' + cy.batch(() => { + cy.elements().remove() + cy.add(mergedElements) }) - layout.run() + const hasSavedPositions = cy + .nodes() + .toArray() + .some((node: NodeSingular) => Boolean(positionsRef.current[node.id()])) + + if (hasSavedPositions) { + cy.layout({ + name: 'preset', + fit: true, + padding: 30, + animate: false + }).run() + } else { + cy.layout({ + name: 'cose', + fit: true, + padding: 30, + animate: false, + randomize: true + }).run() + } + + cy.resize() + cy.fit(undefined, 30) } - /** - * Gets all the related genes to the given gene. - * @param selectedGene Gene object. - */ - const getRelatedGenes = (selectedGene: BiomarkerMolecule) => { - const searchParams = { gene_id: selectedGene.identifier, min_combined_score: minCombinedScore } - - ky.get(urlGeneAssociationsNetwork, { searchParams: searchParams as any, signal: abortControllerTerm.current.signal }).then((response) => { - response.json().then((data: { data: any /* TODO: type */ }) => { - // The response is a CytoscapeElements object already - initCytoscape(data.data) - }).catch((err) => { - alertGeneralError() - console.log('Error parsing JSON ->', err) + const getRelatedGenes = async (gene: BiomarkerMolecule, score: number) => { + abortControllerRef.current?.abort() + abortControllerRef.current = new AbortController() + + try { + const response = await ky.get(urlGeneAssociationsNetwork, { + searchParams: { + gene_id: gene.identifier, + min_combined_score: score + } as any, + signal: abortControllerRef.current.signal }) - }).catch((err) => { - if (!abortControllerTerm.current.signal.aborted) { + + const data = await response.json<{ data: CytoscapeResponseData }>() + applyElements(data.data) + } catch (err) { + if (!abortControllerRef.current?.signal.aborted) { alertGeneralError() } console.log('Error getting experiment', err) - }) + } } useEffect(() => { - getRelatedGenes(props.selectedGene) - }, [props.selectedGene]) + createCy() - /** On unmount, cancels the request */ - useEffect(() => { return () => { - // Cleanup: cancel the ongoing request when component unmounts - abortControllerTerm.current.abort() + abortControllerRef.current?.abort() + cyRef.current?.destroy() + cyRef.current = null } }, []) + useEffect(() => { + if (!cyRef.current) { return } + + getRelatedGenes(selectedGene, minCombinedScore) + }, [selectedGene, minCombinedScore]) + return ( @@ -257,27 +336,41 @@ export const GeneAssociationsNetworkPanel = (props: GeneAssociationsNetworkPanel value={minCombinedScore} min={900} max={1000} - // TODO: implement with debounce onChange={(_e, { value }) => setMinCombinedScore(Number(value))} /> + - The combined score is computed by combining the probabilities from the different evidence channels and corrected for the probability of randomly observing an interaction. For a more detailed description please see von Mering, et al. Nucleic Acids Res. 2005 + The combined score is computed by combining the probabilities from the different evidence channels and corrected for the probability of randomly observing an interaction. For a more detailed description please see{' '} + + von Mering, et al. Nucleic Acids Res. 2005 +
)} onTop={false} onEvent='click' /> + -
+
diff --git a/src/frontend/static/frontend/src/components/biomarkers/types.ts b/src/frontend/static/frontend/src/components/biomarkers/types.ts index 131ae12d..98d2b66f 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/types.ts +++ b/src/frontend/static/frontend/src/components/biomarkers/types.ts @@ -389,6 +389,7 @@ enum ActiveBiomarkerMoleculeItemMenu { PATHWAYS, GENE_ASSOCIATIONS_NETWORK, GENE_ONTOLOGY, + GENE_REGULATION_ASSOCIATIONS, INFERENCE, DISEASES, DRUGS, diff --git a/src/frontend/static/frontend/src/components/differential-expression/VolcanoPlot.tsx b/src/frontend/static/frontend/src/components/differential-expression/VolcanoPlot.tsx index f1a666c9..5be970cd 100644 --- a/src/frontend/static/frontend/src/components/differential-expression/VolcanoPlot.tsx +++ b/src/frontend/static/frontend/src/components/differential-expression/VolcanoPlot.tsx @@ -74,64 +74,64 @@ export const VolcanoPlot = ({ }, ] : [] + return null + // return ( + // d.log2FC), + // y: significant.map((d) => transformP(d.pValue)), + // text: significant.map((d) => d.label ?? d.id), + // mode: 'markers', + // type: 'scattergl', + // name: 'Significant', + // marker: { size: 6 }, + // hovertemplate: + // 'Significant
' + + // 'log2FC: %{x:.2f}
' + + // '-log10(p): %{y:.2f}
' + + // '%{text}' + + // '', + // }, + // { + // x: nonsignificant.map((d) => d.log2FC), + // y: nonsignificant.map((d) => transformP(d.pValue)), + // text: nonsignificant.map((d) => d.label ?? d.id), + // mode: 'markers', + // type: 'scattergl', + // name: 'Not significant', + // marker: { size: 4, opacity: 0.6 }, + // hovertemplate: + // 'Not significant
' + + // 'log2FC: %{x:.2f}
' + + // '-log10(p): %{y:.2f}
' + + // '%{text}' + + // '', + // }, + // ]} + // layout={{ + // title: 'Volcano Plot', + // hovermode: 'closest', + // showlegend: true, + // margin: { l: 90, r: 40, t: 50, b: 70 }, - return ( - d.log2FC), - y: significant.map((d) => transformP(d.pValue)), - text: significant.map((d) => d.label ?? d.id), - mode: 'markers', - type: 'scattergl', - name: 'Significant', - marker: { size: 6 }, - hovertemplate: - 'Significant
' + - 'log2FC: %{x:.2f}
' + - '-log10(p): %{y:.2f}
' + - '%{text}' + - '', - }, - { - x: nonsignificant.map((d) => d.log2FC), - y: nonsignificant.map((d) => transformP(d.pValue)), - text: nonsignificant.map((d) => d.label ?? d.id), - mode: 'markers', - type: 'scattergl', - name: 'Not significant', - marker: { size: 4, opacity: 0.6 }, - hovertemplate: - 'Not significant
' + - 'log2FC: %{x:.2f}
' + - '-log10(p): %{y:.2f}
' + - '%{text}' + - '', - }, - ]} - layout={{ - title: 'Volcano Plot', - hovermode: 'closest', - showlegend: true, - margin: { l: 90, r: 40, t: 50, b: 70 }, - - // Axis labels (normal, not floating) - xaxis: { - title: { text: 'log2(Fold Change)', standoff: 20 }, - zeroline: false, - }, - yaxis: { - title: { text: '-log10(p-value)', standoff: 10 }, - zeroline: false, - }, - shapes: thresholdShapes, - }} - config={{ - responsive: true, - displayModeBar: true, - }} - style={{ width: '100%', height: '500px' }} - /> - ) + // // Axis labels (normal, not floating) + // xaxis: { + // title: { text: 'log2(Fold Change)', standoff: 20 }, + // zeroline: false, + // }, + // yaxis: { + // title: { text: '-log10(p-value)', standoff: 10 }, + // zeroline: false, + // }, + // shapes: thresholdShapes, + // }} + // config={{ + // responsive: true, + // displayModeBar: true, + // }} + // style={{ width: '100%', height: '500px' }} + // /> + // ) } diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx new file mode 100644 index 00000000..a6123bb0 --- /dev/null +++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx @@ -0,0 +1,1606 @@ +import React from 'react' +// Update the import path to the correct location of Base component +import { Modal, DropdownItemProps, Icon, Form, Button, Confirm } from 'semantic-ui-react' +import { DjangoCGDSStudy, DjangoMRNAxGEMResultRow, DjangoSurvivalColumnsTupleSimple, DjangoTag, DjangoUserFile } from '../../../utils/django_interfaces' +import ky, { Options } from 'ky' +import { getDjangoHeader, cleanRef, getFilenameFromSource, getDefaultSource } from '../../../utils/util_functions' +import { NameOfCGDSDataset, Nullable, CustomAlert, CustomAlertTypes, SourceType, ConfirmModal, ExperimentInfo, ExperimentResultTableControl } from '../../../utils/interfaces' +import { Biomarker, BiomarkerType, BiomarkerOrigin, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection, SaveBiomarkerStructure, SaveMoleculeStructure, FeatureSelectionPanelData, SourceStateBiomarker, FeatureSelectionAlgorithm, FitnessFunction, FitnessFunctionParameters, BiomarkerState, AdvancedAlgorithm as AdvancedAlgorithmParameters, BBHAVersion, BiomarkerSimple, CrossValidationParameters } from '../../biomarkers/types' +import { ManualForm } from '../../biomarkers/modalContentBiomarker/manualForm/ManualForm' +import { PaginationCustomFilter } from '../../common/PaginatedTable' +import { isEqual } from 'lodash' +import { getDefaultClusteringParameters, getDefaultRFParameters, getDefaultSvmParameters } from '../../biomarkers/utils' +import { BiomarkerDetailsModal } from '../../biomarkers/BiomarkerDetailsModal' +import { Alert } from '../../common/Alert' + +// URLs defined in gem.html +declare const urlBiomarkersCRUD: string +declare const urlBiomarkersSimpleUpdate: string +declare const urlBiomarkersCreate: string +declare const urlMiRNACodes: string +declare const urlGeneSymbols: string +declare const urlMethylationSites: string +declare const urlMiRNACodesFinder: string +declare const urlMethylationSitesFinder: string +declare const urlGeneSymbolsFinder: string + +const REQUEST_TIMEOUT = 120000 // 2 minutes in milliseconds +type SelectedOption = 'selectAll' | 'selectWithFilters' + +/** A matched molecule with the search query and the validated alias. */ +type MoleculeFinderResult = { molecule: string, standard: string } + +/** Extremely simple struct of a Biomarker (useful for simple updates). */ +type BiomarkerNameAndDesc = { + name: string, + description: string +} + +/** Some flags to validate the Biomarkers form. */ +type ValidationForm = { + haveAmbiguous: boolean, + haveInvalid: boolean +} + +interface BiomarkerFromCorrelationModalProps { + experimentInfo: ExperimentInfo; + tableControl: ExperimentResultTableControl; +} + +/** BiomarkersPanel's state */ +interface BiomarkerFromCorrelationModalState { + biomarkers: BiomarkerSimple[], + newBiomarker: Biomarker, + /** PK of the Biomarker that's being loaded. */ + loadingFullBiomarkerId: Nullable, + selectedBiomarkerToDeleteOrSync: Nullable, + checkedIgnoreProposedAlias: boolean, + showDeleteBiomarkerModal: boolean, + /** Indicates if there's a Biomarker being deleted. */ + deletingBiomarker: boolean, + /** Indicates if there's a Biomarker being stopped. */ + stoppingExperiment: boolean, + /** Biomarker to stop. */ + biomarkerToStop: Nullable, + addingOrEditingBiomarker: boolean, + biomarkerTypeSelected: BiomarkerOrigin, + formBiomarker: FormBiomarkerData, + confirmModal: ConfirmModal + tags: DjangoTag[], + /** Indicates if the modal to create or edit a Biomarker is open. */ + openCreateEditBiomarkerModal: boolean, + /** Indicates if the modal to clone a Biomarker is open. Contains the pk of the Biomarker to clone. */ + biomarkerToClone: Nullable, + /** Indicates if there's a Biomarker being cloned. */ + cloningBiomarker: boolean, + /** Indicates if the modal to get the details of a Biomarker is open. */ + openDetailsModal: boolean, + /** Selected Biomarker instance to show its details. */ + selectedBiomarker: Nullable, + alert: CustomAlert, + featureSelection: FeatureSelectionPanelData, + submittingFSExperiment: boolean, + openDetailsModal2: boolean, + selectedOption: SelectedOption, + openSelectOptionModal: boolean, + experimentInfoWithoutFilters: ExperimentInfo, + modalReady: boolean, +} + +/** + * Renders a CRUD panel for a Biomarker. + */ +export class BiomarkerFromCorrelationModal extends React.Component { + abortController = new AbortController() + constructor (props) { + super(props) + this.state = { + biomarkers: [], + newBiomarker: this.getDefaultNewBiomarker(), + loadingFullBiomarkerId: null, + biomarkerTypeSelected: BiomarkerOrigin.BASE, + checkedIgnoreProposedAlias: false, + showDeleteBiomarkerModal: false, + stoppingExperiment: false, + biomarkerToStop: null, + selectedBiomarkerToDeleteOrSync: null, + deletingBiomarker: false, + addingOrEditingBiomarker: false, + formBiomarker: this.getDefaultFormBiomarker(), + confirmModal: this.getDefaultConfirmModal(), + tags: [], + openCreateEditBiomarkerModal: false, + cloningBiomarker: false, + biomarkerToClone: null, + openDetailsModal: false, + selectedBiomarker: null, + alert: this.getDefaultAlertProps(), + featureSelection: this.getDefaultFeatureSelectionProps(), + submittingFSExperiment: false, + openDetailsModal2: false, + selectedOption: 'selectAll', + openSelectOptionModal: false, + modalReady: false, + experimentInfoWithoutFilters: { + ...props.experimentInfo, + rows: [...props.experimentInfo.rows] + } + } + } + + /** + * Abort controller if component is render + */ + + componentWillUnmount () { + this.abortController.abort() + } + + /** + * Generates default feature selection creation structure + * @returns Default the default Alert + */ + getDefaultFeatureSelectionProps = (): FeatureSelectionPanelData => { + return { + step: 1, + biomarker: null, + selectedBiomarker: null, + clinicalSource: getDefaultSource(), + mRNASource: getDefaultSource(), + mirnaSource: getDefaultSource(), + methylationSource: getDefaultSource(), + cnaSource: getDefaultSource(), + algorithm: FeatureSelectionAlgorithm.BLIND_SEARCH, + fitnessFunction: FitnessFunction.CLUSTERING, + fitnessFunctionParameters: this.getDefaultFitnessFunctionParameters(), + advancedAlgorithmParameters: this.getDefaultAdvancedAlgorithmParameters(), + crossValidationParameters: { folds: 10 } + } + } + + /** + * Handle changes in the checkedIgnoreProposedAlias value. + * @param checkedIgnoreProposedAlias New checkedIgnoreProposedAlias value. + */ + handleChangeIgnoreProposedAlias = (checkedIgnoreProposedAlias: boolean) => { + // Clear all the proposed molecules as they are not valid anymore (they are computed on search only) + const formBiomarker = this.state.formBiomarker + formBiomarker.moleculesSymbolsFinder.data = [] + + this.setState({ checkedIgnoreProposedAlias, formBiomarker }) + } + + /** + * Generates default settings for advance Algorithm data. + * @returns Default structure of all advance algorithms. + */ + getDefaultAdvancedAlgorithmParameters = (): AdvancedAlgorithmParameters => ({ + isActive: false, + BBHA: { + useSpark: true, + numberOfStars: 60, + numberOfIterations: 10, + BBHAVersion: BBHAVersion.ORIGINAL, + coeff1: 2.2, + coeff2: 0.1 + }, + GA: { + useSpark: true, + numberOfIterations: 10, + populationSize: 50, + mutationRate: 0.01 + }, + coxRegression: { + useSpark: true, + topN: 5 + } + }) + + /** + * Generates default settings for all the fitness functions. + * @returns Default structure for all the fitness functions. + */ + getDefaultFitnessFunctionParameters = (): FitnessFunctionParameters => ({ + clusteringParameters: { ...getDefaultClusteringParameters(), lookForOptimalNClusters: false }, // TODO: Change to default when implemented in backend + svmParameters: getDefaultSvmParameters(), + rfParameters: getDefaultRFParameters() + }) + + /** + * Generates a default alert structure + * @returns Default the default Alert + */ + getDefaultAlertProps = (): CustomAlert => { + return { + message: '', // This have to change during cycle of component + isOpen: false, + type: CustomAlertTypes.SUCCESS, + duration: 500 + } + } + + /** + * Generates a default confirm modal structure + * @returns Default confirmModal object + */ + getDefaultConfirmModal = (): ConfirmModal => { + return { + confirmModal: false, + headerText: '', + contentText: '', + onConfirm: () => console.log('DefaultConfirmModalFunction, this should change during cycle of component') + } + } + + handleCloseStopFSExperiment = () => { + this.setState({ biomarkerToStop: null }) + } + + /** + * Reset the confirm modal, to be used again + */ + handleCloseAlert = () => { + const alert = this.state.alert + alert.isOpen = false + this.setState({ alert }) + } + + /** + * Reset the confirm modal, to be used again + */ + handleCancelConfirmModalState () { + this.setState({ confirmModal: this.getDefaultConfirmModal() }) + } + + /** + * Toggle if advance is active or not + */ + handleSwitchAdvanceAlgorithm = () => { + const featureSelection = this.state.featureSelection + featureSelection.advancedAlgorithmParameters.isActive = !featureSelection.advancedAlgorithmParameters.isActive + this.setState({ featureSelection }) + } + + /** + * Change the value of the advance algorithm and prop selected + * @param advanceAlgorithm Advance algorithm selected + * @param name name of prop to change + * @param value value to set + */ + handleChangeAdvanceAlgorithm = (advanceAlgorithm: string, name: string, value: any) => { + const featureSelection = this.state.featureSelection + featureSelection.advancedAlgorithmParameters[advanceAlgorithm][name] = value + this.setState({ featureSelection }) + } + + /** + * Callback when a new file is selected in the uncontrolled component + * (input type=file) + */ + selectNewFile = () => { this.updateSourceFilenamesAndCommonSamples() } + + /** + * Select the algorithm, initialize the state of the selected and clean the others states + * @param algorithm algorithm selected + */ + handleChangeAlgorithm = (algorithm: FeatureSelectionAlgorithm) => { + const featureSelection = this.state.featureSelection + featureSelection.algorithm = algorithm + this.setState({ featureSelection }) + } + + /** + * Select the algorithm, initialize the state of the selected and clean the others states + * @param fitnessFunction Fitness function selected + */ + handleChangeFitnessFunction = (fitnessFunction: FitnessFunction) => { + const featureSelection = this.state.featureSelection + featureSelection.fitnessFunction = fitnessFunction + this.setState({ featureSelection }) + } + + /** + * Manage changes of Feature Selection process parameters. + * @param fitnessFunction name of fitness function to change + * @param key name of the fitnessFunction object that have changed + * @param value value selected typed depends of what fitness function and key is being changing + */ + handleChangeFitnessFunctionOption = (fitnessFunction: T, key: M, value: FitnessFunctionParameters[T][M]) => { + const featureSelection = this.state.featureSelection + featureSelection.fitnessFunctionParameters[fitnessFunction][key] = value + this.setState({ featureSelection }) + } + + /** + * Manage changes of CrossValidation parameters. + * @param key name of the crossValidationParameters object that have changed. + * @param value value selected. + */ + handleChangeCrossValidation = (key: T, value: any) => { + const featureSelection = this.state.featureSelection + featureSelection.crossValidationParameters[key] = value + this.setState({ featureSelection }) + } + + /** + * Selects a CGDS Study as a source + * @param selectedStudy Selected Study as Source + * @param sourceStateName Source's name in state object to update + */ + selectStudy = (selectedStudy: DjangoCGDSStudy, sourceStateName: SourceStateBiomarker) => { + // Selects source to update + const featureSelection = this.state.featureSelection + const source = featureSelection[sourceStateName] + + source.type = SourceType.CGDS + source.CGDSStudy = selectedStudy + this.setState({ featureSelection }, this.updateSourceFilenamesAndCommonSamples) + } + + /** + * Selects a User's file as a source + * @param selectedFile Selected file as Source + * @param sourceStateName Source's name in state object to update + */ + selectUploadedFile = (selectedFile: DjangoUserFile, sourceStateName: SourceStateBiomarker) => { + // Selects source to update + const featureSelection = this.state.featureSelection + const source = featureSelection[sourceStateName] + + source.type = SourceType.UPLOADED_DATASETS + source.selectedExistingFile = selectedFile + this.setState({ featureSelection }, this.updateSourceFilenamesAndCommonSamples) + } + + /** + * Change the source state to submit a pipeline + * @param sourceType New selected Source + * @param sourceStateName Source's name in state object to update + */ + handleChangeSourceType = (sourceType: SourceType, sourceStateName: SourceStateBiomarker) => { + // Selects source to update + const featureSelection = this.state.featureSelection + const source = featureSelection[sourceStateName] + // Change source type + source.type = sourceType + + // Resets all source values + source.selectedExistingFile = null + source.CGDSStudy = null + cleanRef(source.newUploadedFileRef) + + // After update state + this.setState({ featureSelection }, this.updateSourceFilenamesAndCommonSamples) + } + + /** + * Updates Sources' filenames and common examples counter + */ + updateSourceFilenamesAndCommonSamples = () => { + this.updateSourceFilenames() + // this.checkCommonSamples() TODO: check function and dependencies functions in file Pipeline.tsx + } + + /** + * Handles file input changes to set data to show in form + * IMPORTANT: this is necessary because the file inputs are uncontrolled components + * and doesn't trigger an update of the state fields + */ + updateSourceFilenames = () => { + // Updates state filenames + const featureSelection = this.state.featureSelection + featureSelection.mRNASource.filename = getFilenameFromSource(featureSelection.mRNASource) + featureSelection.clinicalSource.filename = getFilenameFromSource(featureSelection.clinicalSource) + featureSelection.cnaSource.filename = getFilenameFromSource(featureSelection.cnaSource) + featureSelection.methylationSource.filename = getFilenameFromSource(featureSelection.methylationSource) + featureSelection.mirnaSource.filename = getFilenameFromSource(featureSelection.mirnaSource) + this.setState({ featureSelection }) + } + + /** + * Changes confirm modal state + * @param setOption New state of option + * @param headerText Optional text of header in confirm modal, by default will be empty + * @param contentText optional text of content in confirm modal, by default will be empty + * @param onConfirm Modal onConfirm callback + */ + handleChangeConfirmModalState = (setOption: boolean, headerText: string, contentText: string, onConfirm: () => void) => { + const confirmModal = this.state.confirmModal + confirmModal.confirmModal = setOption + confirmModal.headerText = headerText + confirmModal.contentText = contentText + confirmModal.onConfirm = onConfirm + this.setState({ confirmModal }) + } + + /** + * Disambiguate the selected molecule for the yellow buttons. + * @param moleculeToDisambiguate Molecule to disambiguate. + * @param section Molecule section. + * @param selectedOption Selected option. + */ + handleSelectOptionMolecule = (moleculeToDisambiguate: MoleculesSectionData, section: BiomarkerType, selectedOption: string) => { + const formBiomarker = this.state.formBiomarker + const indexToSelect = formBiomarker.moleculesSection[section].data.findIndex((item) => isEqual(item.value, moleculeToDisambiguate.value)) + + // Checks if the molecule is already a valid one + const exists = formBiomarker.moleculesSection[section].data.some((item) => item.value === selectedOption) + const data: MoleculesSectionData[] = formBiomarker.moleculesSection[section].data + data.splice(indexToSelect, 1) + + if (!exists) { + data.push({ + isValid: true, + value: selectedOption + }) + } + + this.setState({ + ...this.state, + formBiomarker: { + ...this.state.formBiomarker, + moleculesSection: { + ...this.state.formBiomarker.moleculesSection, + [section]: { + ...this.state.formBiomarker.moleculesSection[section], + data + } + } + } + }) + } + + /** + * Method that select how the user is going to create a Biomarker + * @param type Select the way to create a Biomarker + */ + handleSelectModal = (type: BiomarkerOrigin) => { + this.setState({ biomarkerTypeSelected: type }) + } + + getBiomarkerFullInstance = (biomarkerSimple: BiomarkerSimple): Promise => { + return new Promise((resolve, reject) => { + this.setState({ loadingFullBiomarkerId: biomarkerSimple.id }) + ky.get(urlBiomarkersCRUD + '/' + biomarkerSimple.id + '/', { signal: this.abortController.signal }).then((response) => { + response.json().then((jsonResponse) => { + resolve(jsonResponse) + }).catch((err) => { + console.error('Error parsing JSON on Biomarker retrieval:', err) + reject(err) + }) + }).catch((err) => { + console.error('Error getting Biomarker:', err) + + if (!this.abortController.signal.aborted) { + reject(err) + } + }).finally(() => { + if (!this.abortController.signal.aborted) { + this.setState({ loadingFullBiomarkerId: null }) + } + }) + }) + } + + /** + * Opens the modal to show all the Biomarker details. + * @param selectedBiomarker Selected Biomarker instance. + */ + openBiomarkerDetailsModal = (selectedBiomarker: BiomarkerSimple) => { + this.getBiomarkerFullInstance(selectedBiomarker).then((biomarker) => { + this.setState({ + selectedBiomarker: { + ...selectedBiomarker, + cnas: biomarker.cnas, + mirnas: biomarker.mirnas, + methylations: biomarker.methylations, + mrnas: biomarker.mrnas + }, + openDetailsModal: true + }) + }) + } + + /** Closes the modal of Biomarker's details. */ + closeBiomarkerDetailsModal = () => { + this.setState({ + selectedBiomarker: null, + openDetailsModal: false + }) + } + + /** + * Checks if the user can edit the Biomarker (i.e. It was not used for an Inference experiment, Statistical Validation or Trained Model). + * @param biomarker Biomarker to check. + * @returns True if the Biomarker can be edited, false otherwise. + */ + canEditBiomarker = (biomarker: BiomarkerSimple): boolean => !biomarker.was_already_used && biomarker.state === BiomarkerState.COMPLETED + + /** + * Method that select how the user is going to create a Biomarker + * @param selectedBiomarker Biomarker selected to update + */ + handleOpenEditBiomarker = (selectedBiomarker: BiomarkerSimple) => { + this.getBiomarkerFullInstance(selectedBiomarker).then((biomarker) => { + this.setState({ + biomarkerTypeSelected: BiomarkerOrigin.MANUAL, + openCreateEditBiomarkerModal: true, + formBiomarker: { + id: biomarker.id, + canEditMolecules: this.canEditBiomarker(biomarker), + biomarkerName: biomarker.name, + biomarkerDescription: biomarker.description, + tag: biomarker.tag, + moleculeSelected: BiomarkerType.MRNA, + moleculesTypeOfSelection: MoleculesTypeOfSelection.INPUT, + moleculesSection: { + [BiomarkerType.CNA]: { + isLoading: false, + data: biomarker.cnas.map(item => ({ isValid: true, value: item.identifier })) + }, + [BiomarkerType.MIRNA]: { + isLoading: false, + data: biomarker.mirnas.map(item => ({ isValid: true, value: item.identifier })) + }, + [BiomarkerType.METHYLATION]: { + isLoading: false, + data: biomarker.methylations.map(item => ({ isValid: true, value: item.identifier })) + }, + [BiomarkerType.MRNA]: { + isLoading: false, + data: biomarker.mrnas.map(item => ({ isValid: true, value: item.identifier })) + } + }, + validation: { + haveAmbiguous: false, + haveInvalid: false, + isLoading: false, + checkBox: false + }, + moleculesSymbolsFinder: { + isLoading: false, + data: [] + } + } + }) + }) + } + + handleSelectAllBiomarker = () => { + const allMolecules = this.state.experimentInfoWithoutFilters.rows + + this.setState({ + biomarkerTypeSelected: BiomarkerOrigin.MANUAL, + openCreateEditBiomarkerModal: true, + formBiomarker: { + id: null, + canEditMolecules: true, + biomarkerName: '', + biomarkerDescription: '', + tag: '', + moleculeSelected: BiomarkerType.MRNA, + moleculesTypeOfSelection: MoleculesTypeOfSelection.INPUT, + moleculesSection: { + [BiomarkerType.CNA]: { isLoading: false, data: [] }, + [BiomarkerType.MIRNA]: { + isLoading: false, + data: allMolecules.map(item => ({ + isValid: true, + value: item.gem + })) + }, + [BiomarkerType.METHYLATION]: { isLoading: false, data: [] }, + [BiomarkerType.MRNA]: { + isLoading: false, + data: allMolecules.map(item => ({ + isValid: true, + value: item.gene + })) + } + }, + validation: { + haveAmbiguous: false, + haveInvalid: false, + isLoading: false, + checkBox: false + }, + moleculesSymbolsFinder: { + isLoading: false, + data: [] + } + } + }) + } + + buildFormBiomarkerFromCurrentPageFilteredRows = () => { + const filteredRows = this.getFilteredMoleculesCurrentPage() + const { experimentInfo, tableControl } = this.props + + // Verify if there are filters applied + const hasFilters = Object.values(tableControl.filters).some(f => f.value !== null && f.value !== undefined && f.value !== '') + + // If there are filters applied, use the filtered rows, otherwise use all the rows + const rowsToUse = hasFilters ? filteredRows : experimentInfo.rows + + const genes = rowsToUse.map(r => r.gene) + const gems = rowsToUse.map(r => r.gem) + + return { + ...this.getDefaultFormBiomarker(), + moleculesSection: { + [BiomarkerType.MRNA]: { isLoading: false, data: genes.map(g => ({ value: g, isValid: true })) }, + [BiomarkerType.MIRNA]: { isLoading: false, data: gems.map(g => ({ value: g, isValid: true })) }, + [BiomarkerType.CNA]: { isLoading: false, data: [] }, + [BiomarkerType.METHYLATION]: { isLoading: false, data: [] } + } + } + } + + getFilteredMoleculesCurrentPage = (): DjangoMRNAxGEMResultRow[] => { + const { experimentInfo, tableControl } = this.props + const rows = experimentInfo.rows + + return rows.filter(row => { + return Object.entries(tableControl.filters).every(([key, filter]) => { + const filterValue = filter.value + + if (filterValue === null || filterValue === undefined || filterValue === '') { return true } + + return row[key as keyof DjangoMRNAxGEMResultRow] === filterValue + }) + }) + } + + /** + * Method that get symbols while user is writing in Select molecules input + * @param query string that is sending to the api + */ + handleGenesSymbolsFinder = (query: string): void => { + // loading aca + const formBiomarkerPreLoad = this.state.formBiomarker + formBiomarkerPreLoad.moleculesSymbolsFinder.isLoading = true + let urlToFind = urlGeneSymbolsFinder + + switch (this.state.formBiomarker.moleculeSelected) { + case BiomarkerType.MIRNA: + urlToFind = urlMiRNACodesFinder + break + case BiomarkerType.METHYLATION: + urlToFind = urlMethylationSitesFinder + break + default: + break + } + + this.setState({ formBiomarker: formBiomarkerPreLoad }) + ky.get(urlToFind, { searchParams: { query, limit: 5 }, signal: this.abortController.signal, timeout: REQUEST_TIMEOUT }).then((response) => { + response.json().then((jsonResponse) => { + const formBiomarker = this.state.formBiomarker + const checkedIgnoreProposedAlias = this.state.checkedIgnoreProposedAlias // For short + + formBiomarker.moleculesSymbolsFinder.data = jsonResponse.map(molecule => { + const text = checkedIgnoreProposedAlias || molecule.molecule === molecule.standard + ? molecule.molecule + : `${molecule.molecule} (${molecule.standard})` + + return { + key: molecule.molecule, + text, + value: checkedIgnoreProposedAlias ? molecule.molecule : molecule.standard + } + }) + this.setState({ formBiomarker }) + }).catch((err) => { + console.error('Error parsing JSON ->', err) + }) + }).catch((err) => { + console.error('Error getting genes ->', err) + }).finally(() => { + if (!this.abortController.signal.aborted) { + const formBiomarker = this.state.formBiomarker + formBiomarker.moleculesSymbolsFinder.isLoading = false + this.setState({ formBiomarker }) + } + }) + } + + /** + * Method that removes invalid genes of the sector selected + * @param sector string of the sector selected to change state + */ + handleRemoveInvalidGenes = (sector: BiomarkerType): void => { + this.setState({ + ...this.state, + formBiomarker: { + ...this.state.formBiomarker, + moleculesSection: { + ...this.state.formBiomarker.moleculesSection, + [sector]: { + ...this.state.formBiomarker.moleculesSection[sector], + data: this.state.formBiomarker.moleculesSection[sector].data.filter(gen => gen.isValid || Array.isArray(gen.value)) + } + } + } + }) + } + + /** + * Method that removes all the molecules of the sector selecterd + * @param sector string of the sector selected to change state + */ + handleRestartSection = (sector: BiomarkerType): void => { + this.setState({ + ...this.state, + formBiomarker: { + ...this.state.formBiomarker, + moleculesSection: { + ...this.state.formBiomarker.moleculesSection, + [sector]: { + ...this.state.formBiomarker.moleculesSection[sector], + data: [] + } + } + } + }) + } + + /** + * Order data to show in the section. + * @param data Data to order. + * @returns Ordered data. + */ + orderData = (data: MoleculesSectionData[]): MoleculesSectionData[] => { + return data.sort((a, b) => { + const cond = Number(a.isValid) - Number(b.isValid) + + if (cond !== 0) { + return cond + } + + return Array.isArray(a.value) ? 1 : -1 + }) + } + + /** + * Sets a list of molecules to the current selected section. + * @param moleculesList List of molecules to set. + */ + setMoleculesToSelectedSection = (moleculesList: MoleculesSectionData[]) => { + const moleculeTypeSelected = this.state.formBiomarker.moleculeSelected + + // Sets loading in false + const moleculesSection = { + ...this.state.formBiomarker.moleculesSection, + [moleculeTypeSelected]: { + isLoading: false, + data: this.orderData([...this.state.formBiomarker.moleculesSection[moleculeTypeSelected].data].concat(moleculesList)) + } + } + + const newFormBiomarker: FormBiomarkerData = { + ...this.state.formBiomarker, + moleculesSection + } + + newFormBiomarker.moleculesSymbolsFinder.isLoading = false + newFormBiomarker.moleculesSection[moleculeTypeSelected].isLoading = false + + this.setState({ formBiomarker: newFormBiomarker }) + } + + /** + * Method that gets symbols while user is writing in Select molecules input + * @param molecules array of strings that is sending to the api + */ + handleGeneSymbols = async (molecules: string[]): Promise => { + const moleculesSectionPreload = { + ...this.state.formBiomarker.moleculesSection, + [this.state.formBiomarker.moleculeSelected]: { + isLoading: true, + data: [...this.state.formBiomarker.moleculesSection[this.state.formBiomarker.moleculeSelected].data] + } + } + this.setState({ + formBiomarker: { + ...this.state.formBiomarker, + moleculesSymbolsFinder: { + ...this.state.formBiomarker.moleculesSymbolsFinder, + isLoading: true + }, + moleculesSection: moleculesSectionPreload + } + }) + let urlToFind: string + let json: { [key: string]: string[] } + let keyMolecules: string + + switch (this.state.formBiomarker.moleculeSelected) { + case BiomarkerType.MIRNA: + urlToFind = urlMiRNACodes + json = { mirna_codes: molecules } + keyMolecules = 'mirna_codes' + break + case BiomarkerType.METHYLATION: + urlToFind = urlMethylationSites + json = { methylation_sites: molecules } + keyMolecules = 'methylation_sites' + break + default: + urlToFind = urlGeneSymbols + json = { gene_ids: molecules } + keyMolecules = 'gene_ids' + break + } + + const genesArray: MoleculesSectionData[] = [] + ky.post(urlToFind, { headers: getDjangoHeader(), json, timeout: REQUEST_TIMEOUT }).then((response) => { + response.json<{ [key: string]: string[] }>().then((jsonResponse) => { + const genes = Object.entries(jsonResponse) + + for (const gene of genes) { + let condition + + switch (gene[1].length) { + case 0: + condition = this.state.formBiomarker.moleculesSection[this.state.formBiomarker.moleculeSelected].data.concat(genesArray).filter(item => item.value === gene[0]) + + if (!condition.length) { + genesArray.push({ + isValid: false, + value: gene[0] + }) + } + + break + case 1: + condition = this.state.formBiomarker.moleculesSection[this.state.formBiomarker.moleculeSelected].data.concat(genesArray).filter(item => item.value === gene[1][0]) + + if (!condition.length) { + genesArray.push({ + isValid: true, + value: gene[1][0] + }) + } + + break + default: + condition = this.state.formBiomarker.moleculesSection[this.state.formBiomarker.moleculeSelected].data.concat(genesArray).filter( + item => isEqual(item.value, gene[1]) + ) + + if (!condition.length) { + genesArray.push({ + isValid: false, + value: gene[1] + }) + } + + break + } + } + }).catch((err) => { + console.error('Error parsing JSON ->', err) + console.warn('Setting all molecules as invalid to show warning') + + json[keyMolecules].forEach(molecule => { + genesArray.push({ + isValid: false, + value: molecule + }) + }) + }).finally(() => { + this.setMoleculesToSelectedSection(genesArray) + }) + }).catch((err) => { + console.error('Error getting molecules ->', err) + console.warn('Setting all molecules as invalid to show warning') + + json[keyMolecules].forEach(molecule => { + genesArray.push({ + isValid: false, + value: molecule + }) + }) + }).finally(() => { + this.setMoleculesToSelectedSection(genesArray) + }) + } + + /** + * Generates a default formBiomarker + * @returns Default FormBiomarkerData object + */ + getDefaultFormBiomarker (): FormBiomarkerData { + return { + id: null, + biomarkerName: '', + biomarkerDescription: '', + canEditMolecules: true, + tag: null, + moleculeSelected: BiomarkerType.MRNA, + moleculesTypeOfSelection: MoleculesTypeOfSelection.INPUT, + validation: { + haveAmbiguous: false, + haveInvalid: false, + isLoading: false, + checkBox: false + }, + moleculesSection: { + [BiomarkerType.CNA]: { + isLoading: false, + data: [] + }, + [BiomarkerType.MIRNA]: { + isLoading: false, + data: [] + }, + [BiomarkerType.METHYLATION]: { + isLoading: false, + data: [] + }, + [BiomarkerType.MRNA]: { + isLoading: false, + data: [] + } + }, + moleculesSymbolsFinder: { + isLoading: false, + data: [] + } + } + } + + /** + * Updates checkbox status + * @param value new value to set + */ + handleChangeCheckBox = (value: boolean) => { + const formBiomarker = this.state.formBiomarker + formBiomarker.validation.checkBox = value + this.setState({ formBiomarker }) + } + + /** + * Validates if the form is correct, if not change state of labels alerts bars + * @returns Some flags indicating if the form is valid or not + */ + handleValidateForm = (): ValidationForm => { + let haveAmbiguous = false + let haveInvalid = false + + for (const option of Object.values(BiomarkerType)) { + if (!haveAmbiguous) { + const indexOfAmbiguous = this.state.formBiomarker.moleculesSection[option].data.findIndex(item => !item.isValid && Array.isArray(item.value)) + + if (indexOfAmbiguous >= 0) { + haveAmbiguous = true + } + } + + if (!haveInvalid && !this.state.formBiomarker.validation.checkBox) { + const indexOfInvalid = this.state.formBiomarker.moleculesSection[option].data.findIndex(item => !item.isValid && !Array.isArray(item.value)) + + if (indexOfInvalid >= 0) { + haveInvalid = true + } + } + } + + return { + haveAmbiguous, + haveInvalid + } + } + + /** + * Checks if it's a valid structure to send the molecule to backend and create the Biomarker. + * @param item Molecule to send. + * @returns True if it's valid, false if not. + */ + moleculeIdentifierIsValid = (item: MoleculesSectionData): boolean => !Array.isArray(item.value) && item.isValid + + /** + * Generates a valid structure to send the molecule to backend and create the Biomarker + * @param item Molecule to send + * @returns Correct structure to send + */ + moleculeIdentified = (item: MoleculesSectionData): SaveMoleculeStructure => ({ + identifier: item.value as string + }) + + /** + * Generates a valid structure to send the molecules to backend and create the Biomarker checking if the + * "Ignore errors" checkbox is checked or not. + * @param molecules Molecules to send. + * @returns Correct structure to send. + */ + getMoleculesData = (molecules: MoleculesSectionData[]): SaveMoleculeStructure[] => { + const ignoreErrors = this.state.formBiomarker.validation.checkBox + + if (ignoreErrors) { + return molecules.map(this.moleculeIdentified) + } else { + return molecules.filter(this.moleculeIdentifierIsValid).map(this.moleculeIdentified) + } + } + + /** + * Makes the request to create a Biomarker + */ + handleSendForm = () => { + const formBiomarker = this.state.formBiomarker + formBiomarker.validation.isLoading = true + this.setState({ formBiomarker }) + + // Gets name and description + const simpleBiomarker: BiomarkerNameAndDesc = { + name: formBiomarker.biomarkerName, + description: formBiomarker.biomarkerDescription + } + + // Adds molecules if needed + const biomarkerToSend: SaveBiomarkerStructure | BiomarkerNameAndDesc = formBiomarker.canEditMolecules + ? { + ...simpleBiomarker, + mrnas: this.getMoleculesData(formBiomarker.moleculesSection.mRNA.data), + mirnas: this.getMoleculesData(formBiomarker.moleculesSection.miRNA.data), + cnas: this.getMoleculesData(formBiomarker.moleculesSection.CNA.data), + methylations: this.getMoleculesData(formBiomarker.moleculesSection.Methylation.data) + } + : simpleBiomarker + + const settings: Options = { + headers: getDjangoHeader(), + json: biomarkerToSend, + timeout: REQUEST_TIMEOUT + } + + // Checks if it's a creation or an update + if (!formBiomarker.id) { + ky.post(urlBiomarkersCreate, settings).then((response) => { + response.json().then((_jsonResponse) => { + this.closeModalWithSuccessMsg('Biomarker created successfully') + }).catch((err) => { + console.log('Error parsing JSON ->', err) + }) + }).catch((err) => { + console.log('Error adding Biomarker ->', err) + const alert = this.state.alert + alert.isOpen = true + alert.type = CustomAlertTypes.ERROR + alert.message = 'Error creating biomarker!' + this.setState({ alert }) + }).finally(() => { + formBiomarker.validation.isLoading = false + this.setState({ formBiomarker }) + }) + } else { + const url = formBiomarker.canEditMolecules ? urlBiomarkersCRUD : urlBiomarkersSimpleUpdate + ky.patch(`${url}/${formBiomarker.id}/`, settings).then((response) => { + response.json().then((_jsonResponse) => { + this.closeModalWithSuccessMsg('Biomarker edited successfully') + }).catch((err) => { + console.log('Error parsing JSON ->', err) + }) + }).catch((err) => { + console.log('Error getting genes ->', err) + const alert = this.state.alert + alert.isOpen = true + alert.type = CustomAlertTypes.ERROR + alert.message = 'Error editing biomarker!' + this.setState({ alert }) + }).finally(() => { + formBiomarker.validation.isLoading = false + this.setState({ formBiomarker }) + }) + } + } + + /** + * change name or description of manual form + * @param value new value for input form + * @param name type of input to change + */ + handleChangeInputForm = (value: string, name: 'biomarkerName' | 'biomarkerDescription') => { + const formBiomarker = this.state.formBiomarker + formBiomarker[name] = value + this.setState({ formBiomarker }) + } + + /** + * Handles the table's control filters, select, etc changes + * @param value Value to set to the state moleculeSelected in formBiomarkerState + */ + handleChangeMoleculeSelected = (value: BiomarkerType) => { + const formBiomarker = this.state.formBiomarker + formBiomarker.moleculeSelected = value + formBiomarker.moleculesSymbolsFinder.data = [] + this.setState({ + formBiomarker + }) + } + + /** + * Handles the table's control filters, select, etc changes + * @param value Value to set to the state moleculesTypeOfSelection in formBiomarkerState + */ + handleChangeMoleculeInputSelected = (value: MoleculesTypeOfSelection) => { + this.setState({ + formBiomarker: { + ...this.state.formBiomarker, + moleculesTypeOfSelection: value + } + }) + } + + /** + * Handles the table's control filters, select, etc changes + * @param value Value to add to the molecules section that is selected + */ + handleAddMoleculeToSection = (value: MoleculesSectionData) => { + console.debug('handleAddMoleculeToSection', value) + const genesSymbolsFinder = this.state.formBiomarker.moleculesSymbolsFinder + genesSymbolsFinder.data = [] + this.setState({ + formBiomarker: { + ...this.state.formBiomarker, + moleculesSymbolsFinder: genesSymbolsFinder + } + }) + + const sectionFound = this.state.formBiomarker.moleculesSection[this.state.formBiomarker.moleculeSelected].data.find((item: MoleculesSectionData) => value.value === item.value) + + if (sectionFound !== undefined) { + return + } + + const moleculesSection = { + ...this.state.formBiomarker.moleculesSection, + [this.state.formBiomarker.moleculeSelected]: { + isLoading: false, + data: [...this.state.formBiomarker.moleculesSection[this.state.formBiomarker.moleculeSelected].data, value] + } + } + this.setState({ + formBiomarker: { + ...this.state.formBiomarker, + moleculesSection + } + }) + } + + /** + * Handles the table's control filters, select, etc changes + * @param section Value to add to the molecules section that is selected + * @param molecule molecule to remove of the array + */ + handleRemoveMolecule = (section: BiomarkerType, molecule: MoleculesSectionData) => { + // keeps the molecules that are not the one that is going to be removed + const data = this.state.formBiomarker.moleculesSection[section].data.filter((item: MoleculesSectionData) => { + return item.value !== molecule.value + }) + + this.setState({ + ...this.state, + formBiomarker: { + ...this.state.formBiomarker, + moleculesSection: { + ...this.state.formBiomarker.moleculesSection, + [section]: { + isLoading: false, + data + } + } + } + }) + } + + /** + * Generates a default new file form + * @returns An object with all the field with default values + */ + getDefaultNewBiomarker (): Biomarker { + return { + id: null, + name: '', + description: '', + tag: null, + number_of_mrnas: 0, + number_of_mirnas: 0, + number_of_cnas: 0, + number_of_methylations: 0, + has_fs_experiment: false, + was_already_used: false, + origin: BiomarkerOrigin.BASE, + state: BiomarkerState.COMPLETED, + contains_nan_values: false, + column_used_as_index: '', + methylations: [], + mirnas: [], + cnas: [], + mrnas: [], + is_public: false, + user: { + id: 0, + username: '' + } + } + } + + /** + * Cleans the new/edit biomarker form + */ + cleanForm = () => { + this.setState({ + openCreateEditBiomarkerModal: true, + formBiomarker: this.getDefaultFormBiomarker(), + confirmModal: this.getDefaultConfirmModal() + }) + } + + /** + * Show a modal to confirm a Biomarker deletion + * @param biomarker Selected Biomarker to delete + */ + confirmBiomarkerDeletion = (biomarker: BiomarkerSimple) => { + this.setState({ + selectedBiomarkerToDeleteOrSync: biomarker, + showDeleteBiomarkerModal: true + }) + } + + /** Closes the deletion confirm modals. */ + handleClose = () => { + this.setState({ showDeleteBiomarkerModal: false }) + } + + /** + * Check if can submit the new Biomarker form + * @returns True if everything is OK, false otherwise + */ + canSubmitBiomarkerForm = (): boolean => { + return !this.state.addingOrEditingBiomarker && + this.state.newBiomarker.name.trim().length > 0 + } + + /** + * Handles Biomarker form changes + * @param name Name of the state field to modify + * @param value Value to set to the state field + */ + handleFormChanges = (name: string, value) => { + const newBiomarker = this.state.newBiomarker + newBiomarker[name] = value + this.setState({ newBiomarker }) + } + + /** + * TODO: Check if needed + * Adds a Survival data tuple for a CGDSDataset + * @param datasetName Name of the edited CGDS dataset + */ + addSurvivalFormTuple = (datasetName: NameOfCGDSDataset) => { + const newBiomarker = this.state.newBiomarker + const dataset = newBiomarker[datasetName] + + if (dataset !== null) { + const newElement: DjangoSurvivalColumnsTupleSimple = { event_column: '', time_column: '' } + + if (dataset.survival_columns === undefined) { + dataset.survival_columns = [] + } + + dataset.survival_columns.push(newElement) + this.setState({ newBiomarker }) + } + } + + /** + * TODO: Check if needed + * Removes a Survival data tuple for a CGDSDataset + * @param datasetName Name of the edited CGDS dataset + * @param idxSurvivalTuple Index in survival tuple + */ + removeSurvivalFormTuple = (datasetName: NameOfCGDSDataset, idxSurvivalTuple: number) => { + const newBiomarker = this.state.newBiomarker + const dataset = newBiomarker[datasetName] + + if (dataset !== null && dataset.survival_columns !== undefined) { + dataset.survival_columns.splice(idxSurvivalTuple, 1) + this.setState({ newBiomarker }) + } + } + + /** + * TODO: Check if needed + * Handles CGDS Dataset form changes in fields of Survival data tuples + * @param datasetName Name of the edited CGDS dataset + * @param idxSurvivalTuple Index in survival tuple + * @param name Field of the CGDS dataset to change + * @param value Value to assign to the specified field + */ + handleSurvivalFormDatasetChanges = ( + datasetName: NameOfCGDSDataset, + idxSurvivalTuple: number, + name: string, + value: any + ) => { + const newBiomarker = this.state.newBiomarker + const dataset = newBiomarker[datasetName] + + if (dataset !== null && dataset.survival_columns !== undefined) { + dataset.survival_columns[idxSurvivalTuple][name] = value + this.setState({ newBiomarker }) + } + } + + /** + * Checks if the form is entirely empty. Useful to enable 'Cancel' button + * @returns True is any of the form's field contains any data. False otherwise + */ + isFormEmpty = (): boolean => isEqual(this.state.formBiomarker, this.getDefaultFormBiomarker()) + + /** + * Callback to mark a Biomarker as selected + * @param biomarker Selected biomarker to mark + */ + markBiomarkerAsSelected = (biomarker: Biomarker) => { + const featureSelection = this.state.featureSelection + featureSelection.selectedBiomarker = biomarker + this.setState({ featureSelection }) + } + + /** + * Function to complete step 2 + */ + handleCompleteStep2 = () => { + const featureSelection = this.state.featureSelection + featureSelection.step = 3 + this.setState({ featureSelection }) + } + + /** + * Function to go back to step 1 + */ + handleGoBackStep1 = () => { + const featureSelection = this.state.featureSelection + featureSelection.clinicalSource = getDefaultSource() + featureSelection.mRNASource = getDefaultSource() + featureSelection.mirnaSource = getDefaultSource() + featureSelection.methylationSource = getDefaultSource() + featureSelection.cnaSource = getDefaultSource() + featureSelection.step = 1 + this.setState({ featureSelection }) + } + + /** Closes the modal to confirm a Biomarker cloning. */ + closeModalToClone = () => { this.setState({ biomarkerToClone: null }) } + + /** + * Function to go back to step 2 + */ + handleGoBackStep2 = () => { + const featureSelection = this.state.featureSelection + featureSelection.step = 2 + featureSelection.algorithm = FeatureSelectionAlgorithm.BLIND_SEARCH + this.setState({ featureSelection }) + } + + handleConfirm = () => { + const { selectedOption } = this.state + + this.setState({ openSelectOptionModal: false }, () => { + if (selectedOption === 'selectAll') { + // ver de si hacer un endpoint que traiga todos los genes y no solo los de la pagina + this.handleSelectAllBiomarker() + } else { + // Select with Filters: usamos solo la página actual y filtros + this.setState({ + openCreateEditBiomarkerModal: true, + formBiomarker: this.buildFormBiomarkerFromCurrentPageFilteredRows() + }) + } + }) + } + + /** + * Closes the modal and shows a successful Semantic-UI Alert message. + * @param msg Message to show. + */ + closeModalWithSuccessMsg = (msg: string) => { + const alert = this.state.alert + alert.isOpen = true + alert.type = CustomAlertTypes.SUCCESS + alert.message = msg + this.setState({ + alert, + formBiomarker: this.getDefaultFormBiomarker(), + openCreateEditBiomarkerModal: false, + confirmModal: this.getDefaultConfirmModal(), + biomarkerTypeSelected: BiomarkerOrigin.BASE + }) + } + + /** + * Generates default table's Filters. + * @returns Default object for table's Filters + */ + getDefaultFilters (): PaginationCustomFilter[] { + const tagOptions: DropdownItemProps[] = this.state.tags.map((tag) => { + const id = tag.id as number + return { key: id, value: id, text: tag.name } + }) + + tagOptions.unshift({ key: 'no_tag', text: 'No tag' }) + + // TODO: refactor Tag key as it's the same as AllExperimentsView.tsx and UserFilesView.tsx + return [ + { label: 'Tag', keyForServer: 'tag', defaultValue: '', placeholder: 'Select an existing Tag', options: tagOptions, width: 3 } + ] + } + + closeBiomarkerModal = () => { + this.setState({ + formBiomarker: this.getDefaultFormBiomarker(), + featureSelection: this.getDefaultFeatureSelectionProps(), + openCreateEditBiomarkerModal: false, + confirmModal: this.getDefaultConfirmModal(), + biomarkerTypeSelected: BiomarkerOrigin.BASE, + modalReady: false + }) + } + + buildFormBiomarkerFromFilteredRows = () => { + const { rows } = this.props.experimentInfo + + return { + ...this.getDefaultFormBiomarker(), + moleculesSection: { + [BiomarkerType.MRNA]: { + isLoading: false, + data: rows.map(item => ({ value: item.gene, isValid: true })) + }, + [BiomarkerType.MIRNA]: { + isLoading: false, + data: rows.map(item => ({ value: item.gem, isValid: true })) + }, + // los otros tipos vacíos + [BiomarkerType.CNA]: { isLoading: false, data: [] }, + [BiomarkerType.METHYLATION]: { isLoading: false, data: [] } + } + } + } + + render () { + const { openSelectOptionModal, selectedOption } = this.state + return ( + <> + this.setState({ + formBiomarker: this.getDefaultFormBiomarker(), + openSelectOptionModal: true + })} + /> + + {/* Modal con opciones */} + this.setState({ openSelectOptionModal: false })} + > + Create Biomarker + +

Select how you want to build the biomarker:

+
+ + this.setState({ selectedOption: value as SelectedOption })} + /> + this.setState({ selectedOption: value as SelectedOption })} + /> + +
+
+ + + + +
+ + {/* Modal con ManualForm */} + } + closeOnEscape={false} + closeOnDimmerClick={false} + closeOnDocumentClick={false} + className={this.state.biomarkerTypeSelected !== BiomarkerOrigin.BASE ? 'space-modal large-modal' : undefined} + style={this.state.biomarkerTypeSelected === BiomarkerOrigin.BASE ? { width: '60%', minHeight: '60%' } : undefined} + onClose={() => this.closeBiomarkerModal()} + onOpen={() => { + this.setState({ modalReady: true }, () => { + window.dispatchEvent(new Event('resize')) + }) + }} + > + + + + + {/* Biomarker details modal. */} + } + closeOnEscape={false} + closeOnDimmerClick={false} + closeOnDocumentClick={false} + centered={false} + onClose={this.closeBiomarkerDetailsModal} + open={this.state.openDetailsModal} + > + + + + this.handleCancelConfirmModalState()} + onConfirm={() => this.state.confirmModal.onConfirm()} + /> + + + + + ) + } +} diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx new file mode 100644 index 00000000..79e21bfb --- /dev/null +++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx @@ -0,0 +1,89 @@ +import React, { useState } from 'react' +import { Form, Modal, Button, Icon, ButtonProps } from 'semantic-ui-react' +import { ExperimentInfo } from '../../../utils/interfaces' + +type BiomarkerOption = 'selectAll' | 'selectWithFilters' + +interface CreateBiomarkerButtonProps extends Partial { + experimentInfo: ExperimentInfo + onCreateBiomarker: (options: { + experimentInfo: ExperimentInfo; + selectAll: boolean; + }) => void; +} + +const CreateBiomarkerButton: React.FC = ({ + experimentInfo, + onCreateBiomarker, + ...buttonProps +}) => { + const [open, setOpen] = useState(false) + const [selectedOption, setSelectedOption] = useState('selectAll') + + const handleConfirm = () => { + onCreateBiomarker({ + experimentInfo, + selectAll: selectedOption === 'selectAll', + }) + setOpen(false) + } + + return ( + <> + {/* Button that opens the modal */} + + + {/* Modal with options */} + setOpen(false)} + > + Create Biomarker + +

Select how you want to build the biomarker:

+
+ + setSelectedOption(value as BiomarkerOption)} + /> + setSelectedOption(value as BiomarkerOption)} + /> + +
+
+ + + + +
+ + ) +} + +export default CreateBiomarkerButton diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx index 3f1fd12c..651c5cf1 100644 --- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx +++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx @@ -5,6 +5,7 @@ import { InfoPopup } from './gene-gem-details/InfoPopup' import { generatesOrderingQueryMultiField } from '../../../utils/util_functions' import { DjangoExperiment } from '../../../utils/django_interfaces' import { SingleRangeSlider } from 'neo-react-semantic-ui-range' +import { BiomarkerFromCorrelationModal } from './BiomarkerFromCorrelationModal' declare const urlDownloadResultWithFilters: string @@ -29,9 +30,9 @@ interface ResultTableControlFormProps { /** Callback for changes in (adjusted) p-values precision only */ changePrecisionState: (showHighPrecision: boolean) => void, /** Callback for reset filters and sorting */ - resetFiltersAndSorting: (experiment: DjangoExperiment) => void + resetFiltersAndSorting: (experiment: DjangoExperiment) => void, /** Callback for reset only filters */ - resetFilters: (experiment: ExperimentInfo) => void + resetFilters: (experiment: ExperimentInfo) => void, } /** @@ -70,140 +71,145 @@ export const ResultTableControlForm = (props: ResultTableControlFormProps) => { } return ( -
- - {/* Number of showing/total combinations */} - - - - - - {props.numberOfShowingCombinations} / {props.totalNumberOfCombinations} - - - SHOWING/TOTAL - - - - - - {/* mRNA/MiRNA search */} - props.onHandleTableControlChanges(name, value)} - /> - - {/* Correlation threshold */} - - - - props.onHandleTableControlChanges('coefficientThreshold', value)} - /> - - - - - - {/* Correlation type */} - props.onHandleTableControlChanges(name, value)} - /> - - {/* Page size */} - props.onHandleTableControlChanges(name, value)} - /> - - props.changePrecisionState(!isShowingHighPrecision)} - /> - - props.resetFilters(props.experimentInfo)} - /> - - props.resetFiltersAndSorting(props.experimentInfo.experiment)} - /> - - window.open(generateDownloadWithFiltersQuery(), '_blank')} - disabled={!props.experimentInfo.rows.length} - /> - - - - This table shows all the combinations whose correlation coefficient was more or equal than selected threshold ({props.minimumCoefficientThreshold}). Choose some of the options listed in Actions column -

- )} - /> -
-
-
+ <> +
+ +
+ + {/* Number of showing/total combinations */} + + + + + + {props.numberOfShowingCombinations} / {props.totalNumberOfCombinations} + + + SHOWING/TOTAL + + + + + + {/* mRNA/MiRNA search */} + props.onHandleTableControlChanges(name, value)} + className='no-margin-right-form-field' + /> + + {/* Correlation threshold */} + + + + props.onHandleTableControlChanges('coefficientThreshold', value)} + /> + + + + + + {/* Correlation type */} + props.onHandleTableControlChanges(name, value)} + className='no-margin-right-form-field' + /> + + {/* Page size */} + props.onHandleTableControlChanges(name, value)} + className='no-margin-right-form-field' + /> + + props.changePrecisionState(!isShowingHighPrecision)} + /> + + + + props.resetFiltersAndSorting(props.experimentInfo.experiment)} + /> + + window.open(generateDownloadWithFiltersQuery(), '_blank')} + disabled={!props.experimentInfo.rows.length} + /> + + + + This table shows all the combinations whose correlation coefficient was more or equal than selected threshold ({props.minimumCoefficientThreshold}). Choose some of the options listed in Actions column +

+ )} + /> +
+
+
+
+ + ) } diff --git a/src/frontend/static/frontend/src/css/gem.css b/src/frontend/static/frontend/src/css/gem.css index 27c3edfb..94bb6fb2 100644 --- a/src/frontend/static/frontend/src/css/gem.css +++ b/src/frontend/static/frontend/src/css/gem.css @@ -206,3 +206,90 @@ opacity: 1; text-decoration: underline; } + +.biomarkers--side--bar--box { + margin: 1rem 0; +} + +.biomarkers--side--bar--buttons-group { + width: 100%; +} + +.biomarkers--side--bar--input--selection { + margin: 0.5rem 0; + width: 100%; +} + +.biomarkers--modal--container { + height: 100%; +} + +.large-modal { + min-height: 92% !important; + width: 92% !important; +} + +.space-modal:nth-child(1) { + display: flex !important; + flex-direction: column; + justify-content: space-between; +} + +.biomarkers--side--bar--container { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; +} + +.biomarkers--side--bar--container--item--margin { + margin: 0.5rem 0; +} + +.biomarkers--side--bar--buttons--box { + align-self: flex-end; + display: flex; + justify-self: flex-end; + margin-top: 0.5rem; +} + +.biomarkers--side--bar--validation--items { + margin: 8px 0px 8px 0px; + width: 100%; +} + +.biomarkers--molecules--container { + max-width: 100%; + height: 32vh; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + margin-top: 0.5rem; +} + +.biomarkers--molecules--container--item { + margin: 0.5rem; + flex: 1; + display: flex; + justify-content: center; + align-self: center; +} + +.biomarkers--molecules--container--grid { + width: 100%; +} + +.biomarker--section--icon { + padding: 0 0.5rem; + cursor: pointer; +} + +.biomarker--section--button { + cursor: default !important; +} + +.row-container { + display: flex; + justify-content: center; + align-items: center; + flex-direction: row; +} \ No newline at end of file diff --git a/src/frontend/templates/frontend/gem.html b/src/frontend/templates/frontend/gem.html index b2f1c11c..918fa6a1 100644 --- a/src/frontend/templates/frontend/gem.html +++ b/src/frontend/templates/frontend/gem.html @@ -18,6 +18,15 @@ const downloadFileURL = "{% url 'download_user_file' %}" const urlRunExperiment = "{% url 'run_experiment' %}" const urlUserExperiments = "{% url 'mrna_gem_experiment' %}" + const urlGeneSymbols = "{% url 'gene_symbols' %}" + const urlGeneSymbolsFinder = "{% url 'gene_symbols_finder' %}" + const urlBiomarkersCRUD = "{% url 'biomarkers_api' %}" + const urlBiomarkersSimpleUpdate = "{% url 'biomarkers_api_simple_update' %}" + const urlMiRNACodes = "{% url 'mirna_codes' %}" + const urlMiRNACodesFinder = "{% url 'mirna_codes_finder' %}" + const urlBiomarkersCreate = "{% url 'biomarkers_create' %}" + const urlMethylationSites = "{% url 'methylation_sites' %}" + const urlMethylationSitesFinder = "{% url 'methylation_sites_finder' %}" const urlGetFullUserExperiment = "{% url 'get_full_experiment' %}" const urlGetUsersCandidatesLimited = "{% url 'user_candidates_limited_institution' %}" const urlGetInstitutionsNonInExperiment = "{% url 'institution-non-experiments-list' %}" diff --git a/src/frontend/views.py b/src/frontend/views.py index 7243b6c3..5a9a181f 100644 --- a/src/frontend/views.py +++ b/src/frontend/views.py @@ -1,7 +1,9 @@ from django.shortcuts import render from django.contrib.auth.decorators import login_required from django.conf import settings - +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.shortcuts import render def index_action(request): """Index view""" diff --git a/src/user_files/models.py b/src/user_files/models.py index 35f9ee75..04cf1b24 100644 --- a/src/user_files/models.py +++ b/src/user_files/models.py @@ -15,6 +15,7 @@ from institutions.models import Institution from tags.models import Tag from tissues.models import Tissue +from tissues.models import Tissue from user_files.models_choices import FileType, FileDecimalSeparator from user_files.utils import get_decimal_separator_and_numerical_data, read_excel_in_chunks