From a59a925f3e7fec68c02b6e3d8d26ce8e0b1f41f2 Mon Sep 17 00:00:00 2001 From: Carlos Cruz Date: Wed, 27 Nov 2024 09:49:07 +0000 Subject: [PATCH] [Platform]: Comparative genomics visualisation (#516) Co-authored-by: HelenaCornu Co-authored-by: Chintan Mehta --- .../components/Table/SectionRender.jsx | 6 +- .../static_datasets/prioritisationColumns.ts | 2 + .../components/AssociationsToolkit/types.ts | 1 + .../src/target/ComparativeGenomics/Body.jsx | 14 +- .../ComparativeGenomics/Visualisation.jsx | 278 ++++++++++++++++++ 5 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 packages/sections/src/target/ComparativeGenomics/Visualisation.jsx diff --git a/apps/platform/src/components/AssociationsToolkit/components/Table/SectionRender.jsx b/apps/platform/src/components/AssociationsToolkit/components/Table/SectionRender.jsx index 72303ef55..68ba67aa9 100644 --- a/apps/platform/src/components/AssociationsToolkit/components/Table/SectionRender.jsx +++ b/apps/platform/src/components/AssociationsToolkit/components/Table/SectionRender.jsx @@ -3,6 +3,7 @@ import { styled } from "@mui/material/styles"; import { LoadingBackdrop } from "ui"; import { ENTITIES } from "../../utils"; +import prioritisationColumns from "../../static_datasets/prioritisationColumns"; import targetSections from "../../../../sections/targetSections"; import evidenceSections from "../../../../sections/evidenceSections"; @@ -48,6 +49,7 @@ const getComponentConfig = (displayedTable, row, entity, id, section) => { componentId: entity === ENTITIES.DISEASE ? row.id : id, label: row.original.targetSymbol, entityOfSection: "target", + componentProps: prioritisationColumns.find(el => el.id === section[0])?.sectionProps, }; case "associations": return { @@ -61,6 +63,7 @@ const getComponentConfig = (displayedTable, row, entity, id, section) => { name: row.original.diseaseName, }, entityOfSection: "disease", + componentProps: {}, }; default: return { Component: SectionNotFound }; @@ -87,13 +90,14 @@ export function SectionRender({ componentId, label = row.original[entityToGet][nameProperty], entityOfSection = entity, + componentProps, } = getComponentConfig(displayedTable, row, entity, id, section); if (!Component) return ; return ( - + ); } diff --git a/apps/platform/src/components/AssociationsToolkit/static_datasets/prioritisationColumns.ts b/apps/platform/src/components/AssociationsToolkit/static_datasets/prioritisationColumns.ts index a6f36e34c..3d764c65f 100644 --- a/apps/platform/src/components/AssociationsToolkit/static_datasets/prioritisationColumns.ts +++ b/apps/platform/src/components/AssociationsToolkit/static_datasets/prioritisationColumns.ts @@ -61,6 +61,7 @@ const mouseOrthologMaxIdentityPercentage: Column = { sectionId: "compGenomics", description: "Mouse ortholog maximum identity percentage", docsLink: "https://platform-docs.opentargets.org/target-prioritisation#mouse-ortholog-identity", + sectionProps: { viewMode: "mouseOrthologMaxIdentityPercentage" }, }; const hasHighQualityChemicalProbes: Column = { @@ -124,6 +125,7 @@ const paralogMaxIdentityPercentage: Column = { sectionId: "compGenomics", description: "Paralog maximum identity percentage", docsLink: "https://platform-docs.opentargets.org/target-prioritisation#paralogues", + sectionProps: { viewMode: "paralogMaxIdentityPercentage" }, }; const tissueSpecificity: Column = { diff --git a/apps/platform/src/components/AssociationsToolkit/types.ts b/apps/platform/src/components/AssociationsToolkit/types.ts index a12cc3dd0..a25b1f4f0 100644 --- a/apps/platform/src/components/AssociationsToolkit/types.ts +++ b/apps/platform/src/components/AssociationsToolkit/types.ts @@ -22,6 +22,7 @@ export type Column = { docsLink: string; weight?: number | undefined; private?: boolean; + sectionProps?: any; }; /*************** diff --git a/packages/sections/src/target/ComparativeGenomics/Body.jsx b/packages/sections/src/target/ComparativeGenomics/Body.jsx index 6f0707fcd..fc42b1228 100644 --- a/packages/sections/src/target/ComparativeGenomics/Body.jsx +++ b/packages/sections/src/target/ComparativeGenomics/Body.jsx @@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faStar as faStarSolid } from "@fortawesome/free-solid-svg-icons"; import { faStar } from "@fortawesome/free-regular-svg-icons"; import { SectionItem, Link, Tooltip, OtTable } from "ui"; +import Visualisation from "./Visualisation"; import { definition } from "."; import Description from "./Description"; @@ -24,6 +25,13 @@ import MouseIcon from "./MouseIcon"; import { identifiersOrgLink } from "../../utils/global"; import { decimalPlaces } from "../../constants"; +import { VIEW } from "ui/src/constants"; + +const VIEW_MODES = { + default: "default", + mouseOrthologMaxIdentityPercentage: "mouseOrthologMaxIdentityPercentage", + paralogMaxIdentityPercentage: "paralogMaxIdentityPercentage", +}; // map species ids to species icon component const speciesIcons = { @@ -126,7 +134,7 @@ function getColumns(classes) { ]; } -function Body({ id: ensemblId, label: symbol, entity }) { +function Body({ id: ensemblId, label: symbol, entity, viewMode = VIEW_MODES.default }) { const classes = useStyles(); const variables = { ensemblId }; const request = useQuery(COMP_GENOMICS_QUERY, { variables }); @@ -136,7 +144,11 @@ function Body({ id: ensemblId, label: symbol, entity }) { entity={entity} definition={definition} request={request} + defaultView={VIEW.chart} renderDescription={() => } + renderChart={() => ( + + )} renderBody={() => ( + {viewMode !== "default" && ( + + {content[viewMode]} + + Documentation + + + )} + + + + + ); +} + +const labels = { + 6239: "Caenorhabditis elegans (Nematode, N2)", + 7227: "Drosophila melanogaster (Fruit fly)", + 7955: "Zebrafish", + 8364: "Tropical clawed frog", + 9823: "Pig", + 9615: "Dog", + 10141: "Guinea Pig", + 9986: "Rabbit", + 10116: "Rat", + 10090: "Mouse", + 9544: "Macaque", + 9598: "Chimpanzee", + 9606: "Human", +}; + +function Visualisation({ homologues, width, viewMode }) { + const theme = useTheme(); + const containerReference = useRef(); + const height = 400; + const marginTop = 20; + const marginRight = 20; + const marginBottom = 30; + const marginLeft = 40; + + // Declare the y (vertical position) scale. + const y = d3 + .scalePoint() + .domain(yAxisValues) + .range([height - marginBottom * 2, marginTop]); + + useEffect(() => { + const chartWidth = (width - marginRight) * 0.4; + + // Declare the x (horizontal position) scale. + const queryScale = d3.scaleLinear().domain([0, 100]).range([marginLeft, chartWidth]); + const targetScale = d3 + .scaleLinear() + .domain([100, 0]) + .range([width - chartWidth, width - marginRight]); + + // Create the SVG container. + const svg = d3.create("svg").attr("width", width).attr("height", height); + + // Add the x-axis. + svg + .append("g") + .attr("transform", `translate(0,${height - marginBottom})`) + .call(d3.axisBottom(queryScale)) + .call(g => + g + .append("text") + .attr("x", marginLeft) + .attr("y", marginBottom - 1) + .attr("fill", "currentColor") + .attr("text-anchor", "start") + .text("Query Percentage Identity →") + ); + + // Add the x-axis. + svg + .append("g") + .attr("transform", `translate(0,${height - marginBottom})`) + .call(d3.axisBottom(targetScale)) + .call(g => + g + .append("text") + .attr("x", width - marginRight) + .attr("y", marginBottom - 1) + .attr("fill", "currentColor") + .attr("text-anchor", "end") + .text("← Target Percentage Identity") + ); + + const yAxis = d3 + .axisLeft(y) + .tickFormat(d => labels[d]) + .tickSize(0); + // Add the y-axis. + svg + .append("g") + .attr("class", "queryContainer") + .attr("transform", `translate(${width / 2},0)`) + .style("text-anchor", "middle") + .style("font-size", "0.85rem") + .style("font-weight", 400) + .call(yAxis) + .call(g => g.select(".domain").remove()); + + svg + .append("g") + .attr("stroke", "currentColor") + .attr("stroke-opacity", 0.1) + .call(g => + g + .append("g") + .selectAll("line") + .data(queryScale.ticks()) + .join("line") + .attr("x1", d => queryScale(d)) + .attr("x2", d => queryScale(d)) + .attr("y1", marginTop) + .attr("y2", height - marginBottom) + ) + .call(g => + g + .append("g") + .selectAll("line") + .data(targetScale.ticks()) + .join("line") + .attr("x1", d => targetScale(d)) + .attr("x2", d => targetScale(d)) + .attr("y1", marginTop) + .attr("y2", height - marginBottom) + ); + + // Create the grid + const queryContainer = svg.append("g").attr("class", "queryContainer"); + const targetContainer = svg.append("g").attr("class", "targetContainer"); + + queryContainer + .selectAll(".query") + .data(homologues) + .enter() + .append("circle") + .attr("cx", function (d) { + return queryScale(d.queryPercentageIdentity); + }) + .attr("cy", function (d) { + return y(d.speciesId); + }) + .attr("r", 6) + .attr("fill-opacity", 0.7) + .attr("fill", theme.palette.primary.main) + .attr("stroke", theme.palette.primary.dark); + + targetContainer + .selectAll(".target") + .data(homologues) + .enter() + .append("circle") + .attr("cx", function (d) { + return targetScale(d.targetPercentageIdentity); + }) + .attr("cy", function (d) { + return y(d.speciesId); + }) + .attr("r", 6) + .attr("fill-opacity", 0.7) + .attr("fill", theme.palette.primary.main) + .attr("stroke", theme.palette.primary.dark); + + if (viewMode === "mouseOrthologMaxIdentityPercentage") { + targetContainer.selectAll("circle").attr("fill", grey[300]).attr("stroke", grey[300]); + + queryContainer + .selectAll("circle") + // .transition(300) + .attr("fill", d => + d.queryPercentageIdentity > 80 && d.speciesId == "10090" + ? theme.palette.primary.main + : grey[300] + ) + .attr("stroke", d => + d.queryPercentageIdentity > 80 && d.speciesId == "10090" + ? theme.palette.primary.dark + : grey[300] + ); + queryContainer + .append("line") + .attr("x1", queryScale(80)) + .attr("x2", queryScale(80)) + .attr("y1", marginTop) + .attr("y2", height - marginBottom) + .attr("stroke", "#2e5943"); + + svg + .selectAll(".queryContainer text") + .attr("color", d => (d === "10090" ? theme.palette.text.primary : grey[400])); + } + if (viewMode === "paralogMaxIdentityPercentage") { + queryContainer.selectAll("circle").attr("fill", grey[300]).attr("stroke", grey[300]); + + targetContainer + .selectAll("circle") + // .transition(300) + .attr("fill", d => + d.targetPercentageIdentity > 60 && d.speciesId == "9606" + ? theme.palette.primary.main + : grey[300] + ) + .attr("stroke", d => + d.targetPercentageIdentity > 60 && d.speciesId == "9606" + ? theme.palette.primary.dark + : grey[300] + ); + targetContainer + .append("line") + .attr("x1", targetScale(60)) + .attr("x2", targetScale(60)) + .attr("y1", marginTop) + .attr("y2", height - marginBottom) + .attr("stroke", "#e3a772"); + + svg + .selectAll(".queryContainer text") + .attr("color", d => (d === "9606" ? theme.palette.text.primary : grey[400])); + } + + // Append the SVG element. + containerReference.current.append(svg.node()); + return () => svg.remove(); + }, [homologues, width, viewMode]); + + return
; +} +export default Wrapper;