From 61017d4a8093b19380cb27894f43ae52b548d779 Mon Sep 17 00:00:00 2001 From: Graham McNeill Date: Tue, 18 Aug 2026 13:25:57 +0100 Subject: [PATCH 01/11] add metrics page --- apps/platform/src/App.tsx | 2 ++ .../src/pages/MetricsPage/MetricsPage.tsx | 7 +++++++ .../src/pages/MetricsPage/MetricsPageWrapper.tsx | 16 ++++++++++++++++ apps/platform/src/pages/MetricsPage/index.ts | 1 + 4 files changed, 26 insertions(+) create mode 100644 apps/platform/src/pages/MetricsPage/MetricsPage.tsx create mode 100644 apps/platform/src/pages/MetricsPage/MetricsPageWrapper.tsx create mode 100644 apps/platform/src/pages/MetricsPage/index.ts diff --git a/apps/platform/src/App.tsx b/apps/platform/src/App.tsx index f17cd85cb..191092a9a 100644 --- a/apps/platform/src/App.tsx +++ b/apps/platform/src/App.tsx @@ -16,6 +16,7 @@ import VariantPage from "./pages/VariantPage"; import StudyPage from "./pages/StudyPage"; import CredibleSetPage from "./pages/CredibleSetPage"; import APIPage from "./pages/APIPage"; +import MetricsPage from "./pages/MetricsPage"; import NotFoundPage from "./pages/NotFoundPage"; import ProjectsPage from "./pages/ProjectsPage"; import AnalysisPage from "./pages/AnalysisPage"; @@ -34,6 +35,7 @@ function App(): ReactElement { } /> } /> + } /> Platform Metrics; +} + +export default MetricsPage; diff --git a/apps/platform/src/pages/MetricsPage/MetricsPageWrapper.tsx b/apps/platform/src/pages/MetricsPage/MetricsPageWrapper.tsx new file mode 100644 index 000000000..8c88339e8 --- /dev/null +++ b/apps/platform/src/pages/MetricsPage/MetricsPageWrapper.tsx @@ -0,0 +1,16 @@ +import { Suspense, lazy } from "react"; +import { BasePage, LoadingBackdrop } from "ui"; + +const MetricsPage = lazy(() => import("./MetricsPage")); + +function MetricsPageWrapper() { + return ( + + }> + + + + ); +} + +export default MetricsPageWrapper; diff --git a/apps/platform/src/pages/MetricsPage/index.ts b/apps/platform/src/pages/MetricsPage/index.ts new file mode 100644 index 000000000..7ae3523b1 --- /dev/null +++ b/apps/platform/src/pages/MetricsPage/index.ts @@ -0,0 +1 @@ +export { default } from "./MetricsPageWrapper"; From 464d760128c1d30b7159cdcf4279d73dceb671bb Mon Sep 17 00:00:00 2001 From: Graham McNeill Date: Thu, 20 Aug 2026 09:53:53 +0100 Subject: [PATCH 02/11] add draft headline stats and diseases plot --- .../src/pages/MetricsPage/MetricsPage.tsx | 212 +++++++++- .../pages/MetricsPage/MetricsPageWrapper.tsx | 2 +- .../src/pages/MetricsPage/metrics.csv | 382 ++++++++++++++++++ packages/ot-constants/src/index.ts | 70 ++++ .../variant/GWASCredibleSets/PheWasPlot.tsx | 32 +- 5 files changed, 664 insertions(+), 34 deletions(-) create mode 100644 apps/platform/src/pages/MetricsPage/metrics.csv diff --git a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx index 4210c0346..80b726df3 100644 --- a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx +++ b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx @@ -1,7 +1,215 @@ -import { Typography } from "@mui/material"; +import { useEffect, useState } from "react"; +import { autoType, csv } from "d3"; +import { Box, Card, CardContent, Grid, Typography, useTheme } from "@mui/material"; +import { format } from "d3"; +import * as Plot from "@observablehq/plot"; +import { therapeuticAreas } from "@ot/constants"; +import { ObsPlot } from "ui"; +import metricsCsv from "./metrics.csv?url"; +import { + faBook, + faChartBar, + faDna, + faLink, + faMapPin, + faNetworkWired, + faPrescriptionBottleMedical, + faStethoscope, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; + +type MetricRow = { dataset: string; kind: string; metric: string; group_value: string; value: number }; +type TherapeuticAreaCount = { + name: string; + count: number; + areas?: { name: string; count: number }[]; +}; + +const count = (rows: MetricRow[], dataset: string) => + rows.find((row) => row.dataset === dataset && row.kind === "scalar" && row.metric === "count")?.value ?? 0; + +function DiseaseByTherapeuticArea({ data }: { data: MetricRow[] }) { + const theme = useTheme(); + const areas = data + .filter((row) => row.dataset === "disease" && row.kind === "grouping" && row.metric === "therapeuticArea") + .map((row) => ({ + name: therapeuticAreas[row.group_value] ?? row.group_value, + count: row.value, + })); + const total = count(data, "disease"); + const otherAreas = areas.filter((area) => area.count < total * 0.02).sort((a, b) => b.count - a.count); + const chartData: TherapeuticAreaCount[] = areas + .filter((area) => area.count >= total * 0.02) + .sort((a, b) => b.count - a.count); + + if (otherAreas.length > 0) { + chartData.push({ + name: "Other", + count: otherAreas.reduce((sum, area) => sum + area.count, 0), + areas: otherAreas, + }); + } + + if (chartData.length === 0) return null; + + return ( + + + Diseases by therapeutic area + + area.count} + yTooltip={(area) => area.name} + xAnchorTooltip="adapt" + yAnchorTooltip="adapt" + renderTooltip={renderDiseaseByTherapeuticAreaTooltip} + gapInfo={0} + renderInfo={() => null} + /> + + ); +} + +function renderDiseaseByTherapeuticAreaChart({ + data, + otherData, + width, + height, +}: { + data: TherapeuticAreaCount[]; + otherData?: { textColor: string }; + width?: number; + height: number; +}) { + const plotWidth = Math.max((width ?? 0) - 210 - 24, 0); + const maxCount = Math.max(...data.map((area) => area.count)); + const countLabelWidth = (area: TherapeuticAreaCount) => `${area.count.toLocaleString()}`.length * 7 + 12; + const isInside = (area: TherapeuticAreaCount) => (area.count / maxCount) * plotWidth >= countLabelWidth(area); + const insideData = data.filter(isInside); + const outsideData = data.filter((area) => !isInside(area)); + + return Plot.plot({ + width: width ?? 0, + height, + style: { fontSize: "14px" }, + marginTop: 4, + marginBottom: 4, + marginLeft: 210, + marginRight: 24, + x: { axis: null }, + y: { domain: data.map((area) => area.name), label: null, tickSize: 0, tickPadding: 8, tickFormat: (name) => name }, + marks: [ + Plot.barX(data, { + x: "count", + y: "name", + fill: (area) => (area.areas ? "#5b89b0" : "#1963a3"), + insetTop: 3, + insetBottom: 3, + className: "obs-tooltip", + }), + Plot.text(insideData, { + x: (area) => area.count, + y: "name", + text: (area) => area.count.toLocaleString(), + textAnchor: "end", + dx: -6, + fill: "white", + lineAnchor: "middle", + fontSize: 13, + className: "obs-tooltip", + }), + Plot.text(outsideData, { + x: (area) => area.count, + y: "name", + text: (area) => area.count.toLocaleString(), + textAnchor: "start", + dx: 6, + fill: otherData?.textColor ?? "currentColor", + lineAnchor: "middle", + fontSize: 13, + className: "obs-tooltip", + }), + ], + }); +} + +function renderDiseaseByTherapeuticAreaTooltip(area: TherapeuticAreaCount) { + if (!area.areas) return null; + + return ( + + {area.areas?.map((otherArea) => ( + + {otherArea.name}: {otherArea.count.toLocaleString()} + + ))} + + ); +} function MetricsPage() { - return Platform Metrics; + const [data, setData] = useState([]); + + useEffect(() => { + csv(metricsCsv, autoType).then((d) => { + setData(d as unknown as MetricRow[]); + }); + }, []); + + return ( + <> + Data Metrics + + + {[ + ["Targets", faDna, count(data, "target")], + ["Diseases", faStethoscope, count(data, "disease")], + ["Drugs", faPrescriptionBottleMedical, count(data, "drug_molecule")], + ["Studies", faChartBar, count(data, "study")], + ["Credible sets", faBook, count(data, "credible_set")], + ["Evidence", faLink, data.filter((row) => row.dataset.startsWith("evidence_") && row.metric === "count").reduce((sum, row) => sum + row.value, 0)], + ["Variants", faMapPin, count(data, "variant")], + ["Prioritised genes", faDna, data.find((row) => row.dataset === "l2g_prediction" && row.kind === "filter" && row.metric === "prioritised_genes")?.value ?? 0], + ["GWAS/GWAS colocs", faNetworkWired, data.find((row) => row.dataset === "colocalisation" && row.group_value === "gwas-gwas")?.value ?? 0], + ["GWAS/QTL colocs", faNetworkWired, data.find((row) => row.dataset === "colocalisation" && row.group_value === "gwas-eqtl")?.value ?? 0], + ].map(([label, icon, value]) => ( + + + + + {label} + {format("~s")(value as number)} + + + + ))} + + Polish: capitalise titles, round numbers, use correct icons, make less like buttons(?) + Alternative: more hierarchical, e.g. split into top-level entity counts then evidence linking targets and diseases, credible sets and colocs in variants section. + + Coverage + + Polish: bars are so 'strong' - maybe ok if will be 1/2 page otherwise consider outline bars + Alternative:Replace Other with 'show more'? + + + + ); } export default MetricsPage; diff --git a/apps/platform/src/pages/MetricsPage/MetricsPageWrapper.tsx b/apps/platform/src/pages/MetricsPage/MetricsPageWrapper.tsx index 8c88339e8..925da6d54 100644 --- a/apps/platform/src/pages/MetricsPage/MetricsPageWrapper.tsx +++ b/apps/platform/src/pages/MetricsPage/MetricsPageWrapper.tsx @@ -5,7 +5,7 @@ const MetricsPage = lazy(() => import("./MetricsPage")); function MetricsPageWrapper() { return ( - + }> diff --git a/apps/platform/src/pages/MetricsPage/metrics.csv b/apps/platform/src/pages/MetricsPage/metrics.csv new file mode 100644 index 000000000..045d0c13c --- /dev/null +++ b/apps/platform/src/pages/MetricsPage/metrics.csv @@ -0,0 +1,382 @@ +run,dataset,kind,metric,expression,group_value,value +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,europepmc,2510811 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,gwas_credible_sets,949600 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,impc,838318 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,expression_atlas,166503 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,cancer_gene_census,91572 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,clinical_precedence,66282 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,eva,41347 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,genomics_england,12536 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,crispr_screen,10960 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,gene_burden,7729 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,orphanet,7232 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,uniprot_literature,6574 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,uniprot_variants,5126 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,gene2phenotype,4915 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,clingen,3824 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,reactome,2928 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,intogen,2595 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,eva_somatic,2373 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,crispr,517 +26.03-test5,association_by_datasource_direct,grouping,datasource,aggregationValue,cancer_biomarkers,469 +26.03-test5,association_by_datasource_direct,scalar,count,,,4732211 +26.03-test5,association_by_datasource_direct,scalar,file_size,,,655362472 +26.03-test5,association_by_datasource_direct,scalar,number_of_partitions,,,53 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,europepmc,8246446 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,gwas_credible_sets,2622428 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,impc,2461182 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,cancer_gene_census,382883 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,clinical_precedence,259530 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,eva,228202 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,expression_atlas,166503 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,genomics_england,99619 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,orphanet,74195 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,uniprot_literature,73310 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,uniprot_variants,57366 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,crispr_screen,50614 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,gene2phenotype,45286 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,clingen,39587 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,intogen,28827 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,eva_somatic,27621 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,gene_burden,25546 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,reactome,19537 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,crispr,7486 +26.03-test5,association_by_datasource_indirect,grouping,datasource,aggregationValue,cancer_biomarkers,5243 +26.03-test5,association_by_datasource_indirect,scalar,count,,,14921411 +26.03-test5,association_by_datasource_indirect,scalar,file_size,,,2579684447 +26.03-test5,association_by_datasource_indirect,scalar,number_of_partitions,,,206 +26.03-test5,association_by_datatype_direct,grouping,datatype,aggregationValue,literature,2510811 +26.03-test5,association_by_datatype_direct,grouping,datatype,aggregationValue,genetic_association,997062 +26.03-test5,association_by_datatype_direct,grouping,datatype,aggregationValue,animal_model,838318 +26.03-test5,association_by_datatype_direct,grouping,datatype,aggregationValue,rna_expression,166503 +26.03-test5,association_by_datatype_direct,grouping,datatype,aggregationValue,somatic_mutation,94789 +26.03-test5,association_by_datatype_direct,grouping,datatype,aggregationValue,known_drug,66282 +26.03-test5,association_by_datatype_direct,grouping,datatype,aggregationValue,genetic_literature,17323 +26.03-test5,association_by_datatype_direct,grouping,datatype,aggregationValue,affected_pathway,14820 +26.03-test5,association_by_datatype_direct,scalar,count,,,4705908 +26.03-test5,association_by_datatype_direct,scalar,file_size,,,651994922 +26.03-test5,association_by_datatype_direct,scalar,number_of_partitions,,,52 +26.03-test5,association_by_datatype_indirect,grouping,datatype,aggregationValue,literature,8246446 +26.03-test5,association_by_datatype_indirect,grouping,datatype,aggregationValue,genetic_association,2823514 +26.03-test5,association_by_datatype_indirect,grouping,datatype,aggregationValue,animal_model,2461182 +26.03-test5,association_by_datatype_indirect,grouping,datatype,aggregationValue,somatic_mutation,406376 +26.03-test5,association_by_datatype_indirect,grouping,datatype,aggregationValue,known_drug,259530 +26.03-test5,association_by_datatype_indirect,grouping,datatype,aggregationValue,rna_expression,166503 +26.03-test5,association_by_datatype_indirect,grouping,datatype,aggregationValue,genetic_literature,123072 +26.03-test5,association_by_datatype_indirect,grouping,datatype,aggregationValue,affected_pathway,80993 +26.03-test5,association_by_datatype_indirect,scalar,count,,,14567616 +26.03-test5,association_by_datatype_indirect,scalar,file_size,,,2521892963 +26.03-test5,association_by_datatype_indirect,scalar,number_of_partitions,,,200 +26.03-test5,association_overall_direct,scalar,count,,,4508002 +26.03-test5,association_overall_direct,scalar,file_size,,,633763096 +26.03-test5,association_overall_direct,scalar,number_of_partitions,,,43 +26.03-test5,association_overall_indirect,scalar,count,,,12466856 +26.03-test5,association_overall_indirect,scalar,file_size,,,2217540168 +26.03-test5,association_overall_indirect,scalar,number_of_partitions,,,143 +26.03-test5,biosample,scalar,count,,,35744 +26.03-test5,biosample,scalar,file_size,,,8254341 +26.03-test5,biosample,scalar,number_of_partitions,,,1 +26.03-test5,clinical_indication,scalar,count,,,53950 +26.03-test5,clinical_indication,scalar,file_size,,,3453490 +26.03-test5,clinical_indication,scalar,number_of_partitions,,,1 +26.03-test5,clinical_report,grouping,clinicalStage,clinicalStage,PHASE_2,63105 +26.03-test5,clinical_report,grouping,clinicalStage,clinicalStage,PHASE_1,49053 +26.03-test5,clinical_report,grouping,clinicalStage,clinicalStage,UNKNOWN,39393 +26.03-test5,clinical_report,grouping,clinicalStage,clinicalStage,PHASE_3,38277 +26.03-test5,clinical_report,grouping,clinicalStage,clinicalStage,PHASE_4,29917 +26.03-test5,clinical_report,grouping,clinicalStage,clinicalStage,APPROVAL,29502 +26.03-test5,clinical_report,grouping,clinicalStage,clinicalStage,PHASE_1_2,15701 +26.03-test5,clinical_report,grouping,clinicalStage,clinicalStage,PHASE_2_3,6106 +26.03-test5,clinical_report,grouping,clinicalStage,clinicalStage,EARLY_PHASE_1,4703 +26.03-test5,clinical_report,grouping,clinicalStage,clinicalStage,IND,4626 +26.03-test5,clinical_report,grouping,clinicalStage,clinicalStage,PRECLINICAL,3812 +26.03-test5,clinical_report,grouping,clinicalStage,clinicalStage,WITHDRAWAL,858 +26.03-test5,clinical_report,grouping,clinicalStage,clinicalStage,PREAPPROVAL,160 +26.03-test5,clinical_report,scalar,count,,,285213 +26.03-test5,clinical_report,scalar,file_size,,,74933170 +26.03-test5,clinical_report,scalar,number_of_partitions,,,1 +26.03-test5,clinical_target,scalar,count,,,13407 +26.03-test5,clinical_target,scalar,file_size,,,2776591 +26.03-test5,clinical_target,scalar,number_of_partitions,,,1 +26.03-test5,colocalisation,grouping,studyTypePair,"concat('gwas-', rightStudyType)",gwas-gwas,167048267 +26.03-test5,colocalisation,grouping,studyTypePair,"concat('gwas-', rightStudyType)",gwas-eqtl,28944408 +26.03-test5,colocalisation,grouping,studyTypePair,"concat('gwas-', rightStudyType)",gwas-tuqtl,8500302 +26.03-test5,colocalisation,grouping,studyTypePair,"concat('gwas-', rightStudyType)",gwas-pqtl,6264043 +26.03-test5,colocalisation,grouping,studyTypePair,"concat('gwas-', rightStudyType)",gwas-sqtl,4706298 +26.03-test5,colocalisation,grouping,studyTypePair,"concat('gwas-', rightStudyType)",gwas-sceqtl,2450996 +26.03-test5,colocalisation,scalar,count,,,217914314 +26.03-test5,colocalisation,scalar,file_size,,,17493948221 +26.03-test5,colocalisation,scalar,number_of_partitions,,,200 +26.03-test5,credible_set,grouping,studyType,studyType,gwas,1446959 +26.03-test5,credible_set,grouping,studyType,studyType,eqtl,1349418 +26.03-test5,credible_set,grouping,studyType,studyType,tuqtl,384849 +26.03-test5,credible_set,grouping,studyType,studyType,sqtl,223500 +26.03-test5,credible_set,grouping,studyType,studyType,sceqtl,52738 +26.03-test5,credible_set,grouping,studyType,studyType,pqtl,33718 +26.03-test5,credible_set,scalar,count,,,3491182 +26.03-test5,credible_set,scalar,file_size,,,3187753665 +26.03-test5,credible_set,scalar,number_of_partitions,,,200 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,EFO_0001444,25050 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,OTAR_0000018,11779 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,EFO_0000651,6143 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,EFO_0000618,4680 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,MONDO_0045024,3692 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,OTAR_0000006,3237 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,OTAR_0000020,2211 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,EFO_0010285,1490 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,EFO_0001379,1467 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,MONDO_0024458,1455 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,OTAR_0000017,1358 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,EFO_0010282,1348 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,EFO_0000540,1289 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,EFO_0000319,1221 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,EFO_0005803,1160 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,EFO_0005741,891 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,GO_0008150,812 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,EFO_0009690,763 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,OTAR_0000010,602 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,MONDO_0002025,370 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,OTAR_0000009,151 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,OTAR_0000014,75 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,MONDO_0021205,71 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,EFO_0002571,61 +26.03-test5,disease,grouping,therapeuticArea,therapeuticAreas,EFO_0005932,61 +26.03-test5,disease,scalar,count,,,47030 +26.03-test5,disease,scalar,file_size,,,7312633 +26.03-test5,disease,scalar,number_of_partitions,,,1 +26.03-test5,disease_hpo,scalar,count,,,31529 +26.03-test5,disease_hpo,scalar,file_size,,,1804225 +26.03-test5,disease_hpo,scalar,number_of_partitions,,,1 +26.03-test5,disease_phenotype,scalar,count,,,180981 +26.03-test5,disease_phenotype,scalar,file_size,,,7930460 +26.03-test5,disease_phenotype,scalar,number_of_partitions,,,1 +26.03-test5,drug_mechanism_of_action,scalar,count,,,6505 +26.03-test5,drug_mechanism_of_action,scalar,file_size,,,579870 +26.03-test5,drug_mechanism_of_action,scalar,number_of_partitions,,,2 +26.03-test5,drug_molecule,grouping,clinicalStage,maximumClinicalStage,UNKNOWN,10771 +26.03-test5,drug_molecule,grouping,clinicalStage,maximumClinicalStage,APPROVAL,5200 +26.03-test5,drug_molecule,grouping,clinicalStage,maximumClinicalStage,PHASE_2,2575 +26.03-test5,drug_molecule,grouping,clinicalStage,maximumClinicalStage,PHASE_3,1454 +26.03-test5,drug_molecule,grouping,clinicalStage,maximumClinicalStage,PHASE_1,1339 +26.03-test5,drug_molecule,grouping,clinicalStage,maximumClinicalStage,PHASE_1_2,411 +26.03-test5,drug_molecule,grouping,clinicalStage,maximumClinicalStage,PHASE_2_3,213 +26.03-test5,drug_molecule,grouping,clinicalStage,maximumClinicalStage,IND,129 +26.03-test5,drug_molecule,grouping,clinicalStage,maximumClinicalStage,PRECLINICAL,60 +26.03-test5,drug_molecule,grouping,clinicalStage,maximumClinicalStage,EARLY_PHASE_1,44 +26.03-test5,drug_molecule,grouping,clinicalStage,maximumClinicalStage,PREAPPROVAL,34 +26.03-test5,drug_molecule,scalar,count,,,22230 +26.03-test5,drug_molecule,scalar,file_size,,,2679269 +26.03-test5,drug_molecule,scalar,number_of_partitions,,,5 +26.03-test5,drug_warning,scalar,count,,,2302 +26.03-test5,drug_warning,scalar,file_size,,,250932 +26.03-test5,drug_warning,scalar,number_of_partitions,,,1 +26.03-test5,enhancer_to_gene,scalar,count,,,48810390 +26.03-test5,enhancer_to_gene,scalar,file_size,,,2647632759 +26.03-test5,enhancer_to_gene,scalar,number_of_partitions,,,30 +26.03-test5,evidence_cancer_biomarkers,grouping,datatype,datatypeId,affected_pathway,1301 +26.03-test5,evidence_cancer_biomarkers,scalar,count,,,1301 +26.03-test5,evidence_cancer_biomarkers,scalar,file_size,,,122065 +26.03-test5,evidence_cancer_biomarkers,scalar,number_of_partitions,,,1 +26.03-test5,evidence_cancer_gene_census,grouping,datatype,datatypeId,somatic_mutation,91572 +26.03-test5,evidence_cancer_gene_census,scalar,count,,,91572 +26.03-test5,evidence_cancer_gene_census,scalar,file_size,,,5971585 +26.03-test5,evidence_cancer_gene_census,scalar,number_of_partitions,,,5 +26.03-test5,evidence_clingen,grouping,datatype,datatypeId,genetic_literature,3894 +26.03-test5,evidence_clingen,scalar,count,,,3894 +26.03-test5,evidence_clingen,scalar,file_size,,,509278 +26.03-test5,evidence_clingen,scalar,number_of_partitions,,,1 +26.03-test5,evidence_clinical_precedence,grouping,datatype,datatypeId,clinical,599344 +26.03-test5,evidence_clinical_precedence,scalar,count,,,599344 +26.03-test5,evidence_clinical_precedence,scalar,file_size,,,47458531 +26.03-test5,evidence_clinical_precedence,scalar,number_of_partitions,,,11 +26.03-test5,evidence_crispr,grouping,datatype,datatypeId,affected_pathway,517 +26.03-test5,evidence_crispr,scalar,count,,,517 +26.03-test5,evidence_crispr,scalar,file_size,,,70147 +26.03-test5,evidence_crispr,scalar,number_of_partitions,,,1 +26.03-test5,evidence_crispr_screen,grouping,datatype,datatypeId,affected_pathway,21659 +26.03-test5,evidence_crispr_screen,scalar,count,,,21659 +26.03-test5,evidence_crispr_screen,scalar,file_size,,,1563599 +26.03-test5,evidence_crispr_screen,scalar,number_of_partitions,,,4 +26.03-test5,evidence_europepmc,grouping,datatype,datatypeId,literature,24376290 +26.03-test5,evidence_europepmc,scalar,count,,,24376290 +26.03-test5,evidence_europepmc,scalar,file_size,,,8671824871 +26.03-test5,evidence_europepmc,scalar,number_of_partitions,,,200 +26.03-test5,evidence_eva,grouping,datatype,datatypeId,genetic_association,4035263 +26.03-test5,evidence_eva,scalar,count,,,4035263 +26.03-test5,evidence_eva,scalar,file_size,,,452083288 +26.03-test5,evidence_eva,scalar,number_of_partitions,,,22 +26.03-test5,evidence_eva_somatic,grouping,datatype,datatypeId,somatic_mutation,9366 +26.03-test5,evidence_eva_somatic,scalar,count,,,9366 +26.03-test5,evidence_eva_somatic,scalar,file_size,,,1099065 +26.03-test5,evidence_eva_somatic,scalar,number_of_partitions,,,3 +26.03-test5,evidence_expression_atlas,grouping,datatype,datatypeId,rna_expression,237329 +26.03-test5,evidence_expression_atlas,scalar,count,,,237329 +26.03-test5,evidence_expression_atlas,scalar,file_size,,,16703575 +26.03-test5,evidence_expression_atlas,scalar,number_of_partitions,,,5 +26.03-test5,evidence_gene2phenotype,grouping,datatype,datatypeId,genetic_literature,5026 +26.03-test5,evidence_gene2phenotype,scalar,count,,,5026 +26.03-test5,evidence_gene2phenotype,scalar,file_size,,,562314 +26.03-test5,evidence_gene2phenotype,scalar,number_of_partitions,,,1 +26.03-test5,evidence_gene_burden,grouping,datatype,datatypeId,genetic_association,40444 +26.03-test5,evidence_gene_burden,scalar,count,,,40444 +26.03-test5,evidence_gene_burden,scalar,file_size,,,4585295 +26.03-test5,evidence_gene_burden,scalar,number_of_partitions,,,5 +26.03-test5,evidence_genomics_england,grouping,datatype,datatypeId,genetic_literature,46905 +26.03-test5,evidence_genomics_england,scalar,count,,,46905 +26.03-test5,evidence_genomics_england,scalar,file_size,,,6236722 +26.03-test5,evidence_genomics_england,scalar,number_of_partitions,,,5 +26.03-test5,evidence_gwas_credible_sets,grouping,datatype,datatypeId,genetic_association,3029759 +26.03-test5,evidence_gwas_credible_sets,scalar,count,,,3029759 +26.03-test5,evidence_gwas_credible_sets,scalar,file_size,,,289413549 +26.03-test5,evidence_gwas_credible_sets,scalar,number_of_partitions,,,8 +26.03-test5,evidence_impc,grouping,datatype,datatypeId,animal_model,1522457 +26.03-test5,evidence_impc,scalar,count,,,1522457 +26.03-test5,evidence_impc,scalar,file_size,,,227703094 +26.03-test5,evidence_impc,scalar,number_of_partitions,,,23 +26.03-test5,evidence_intogen,grouping,datatype,datatypeId,somatic_mutation,4223 +26.03-test5,evidence_intogen,scalar,count,,,4223 +26.03-test5,evidence_intogen,scalar,file_size,,,319893 +26.03-test5,evidence_intogen,scalar,number_of_partitions,,,1 +26.03-test5,evidence_orphanet,grouping,datatype,datatypeId,genetic_association,7245 +26.03-test5,evidence_orphanet,scalar,count,,,7245 +26.03-test5,evidence_orphanet,scalar,file_size,,,735287 +26.03-test5,evidence_orphanet,scalar,number_of_partitions,,,1 +26.03-test5,evidence_reactome,grouping,datatype,datatypeId,affected_pathway,10748 +26.03-test5,evidence_reactome,scalar,count,,,10748 +26.03-test5,evidence_reactome,scalar,file_size,,,1078355 +26.03-test5,evidence_reactome,scalar,number_of_partitions,,,4 +26.03-test5,evidence_uniprot_literature,grouping,datatype,datatypeId,genetic_literature,6682 +26.03-test5,evidence_uniprot_literature,scalar,count,,,6682 +26.03-test5,evidence_uniprot_literature,scalar,file_size,,,830649 +26.03-test5,evidence_uniprot_literature,scalar,number_of_partitions,,,1 +26.03-test5,evidence_uniprot_variants,grouping,datatype,datatypeId,genetic_association,36814 +26.03-test5,evidence_uniprot_variants,scalar,count,,,36814 +26.03-test5,evidence_uniprot_variants,scalar,file_size,,,3534449 +26.03-test5,evidence_uniprot_variants,scalar,number_of_partitions,,,5 +26.03-test5,expression,scalar,count,,,50482 +26.03-test5,expression,scalar,file_size,,,42092909 +26.03-test5,expression,scalar,number_of_partitions,,,4 +26.03-test5,go,scalar,count,,,48251 +26.03-test5,go,scalar,file_size,,,745909 +26.03-test5,go,scalar,number_of_partitions,,,1 +26.03-test5,interaction,scalar,count,,,14618053 +26.03-test5,interaction,scalar,file_size,,,91031424 +26.03-test5,interaction,scalar,number_of_partitions,,,8 +26.03-test5,interaction_evidence,scalar,count,,,27432165 +26.03-test5,interaction_evidence,scalar,file_size,,,283743276 +26.03-test5,interaction_evidence,scalar,number_of_partitions,,,200 +26.03-test5,l2g_prediction,filter,prioritised_genes,distinct geneId where score > 0.5,,15509 +26.03-test5,l2g_prediction,scalar,count,,,2794835 +26.03-test5,l2g_prediction,scalar,file_size,,,548945225 +26.03-test5,l2g_prediction,scalar,number_of_partitions,,,200 +26.03-test5,literature,scalar,count,,,163972906 +26.03-test5,literature,scalar,file_size,,,2447297685 +26.03-test5,literature,scalar,number_of_partitions,,,334 +26.03-test5,literature_vector,scalar,count,,,58057 +26.03-test5,literature_vector,scalar,file_size,,,37038512 +26.03-test5,literature_vector,scalar,number_of_partitions,,,1 +26.03-test5,mouse_phenotype,scalar,count,,,210538 +26.03-test5,mouse_phenotype,scalar,file_size,,,10863568 +26.03-test5,mouse_phenotype,scalar,number_of_partitions,,,5 +26.03-test5,openfda_significant_adverse_drug_reactions,scalar,count,,,115698 +26.03-test5,openfda_significant_adverse_drug_reactions,scalar,file_size,,,1782397 +26.03-test5,openfda_significant_adverse_drug_reactions,scalar,number_of_partitions,,,1 +26.03-test5,pharmacogenomics,scalar,count,,,33080 +26.03-test5,pharmacogenomics,scalar,file_size,,,3507000 +26.03-test5,pharmacogenomics,scalar,number_of_partitions,,,5 +26.03-test5,so,scalar,count,,,2615 +26.03-test5,so,scalar,file_size,,,26378 +26.03-test5,so,scalar,number_of_partitions,,,1 +26.03-test5,study,grouping,datasource,projectId,GTEx,1070069 +26.03-test5,study,grouping,datasource,projectId,GCST,133923 +26.03-test5,study,grouping,datasource,projectId,Schmiedel_2018,74874 +26.03-test5,study,grouping,datasource,projectId,Quach_2016,73699 +26.03-test5,study,grouping,datasource,projectId,TwinsUK,68300 +26.03-test5,study,grouping,datasource,projectId,FUSION,64646 +26.03-test5,study,grouping,datasource,projectId,BLUEPRINT,59824 +26.03-test5,study,grouping,datasource,projectId,BrainSeq,36104 +26.03-test5,study,grouping,datasource,projectId,ROSMAP,35088 +26.03-test5,study,grouping,datasource,projectId,CommonMind,31235 +26.03-test5,study,grouping,datasource,projectId,GEUVADIS,26866 +26.03-test5,study,grouping,datasource,projectId,Lepik_2017,26603 +26.03-test5,study,grouping,datasource,projectId,HipSci,25743 +26.03-test5,study,grouping,datasource,projectId,GENCORD,25713 +26.03-test5,study,grouping,datasource,projectId,Alasoo_2018,24550 +26.03-test5,study,grouping,datasource,projectId,Nedelec_2016,22950 +26.03-test5,study,grouping,datasource,projectId,CAP,19518 +26.03-test5,study,grouping,datasource,projectId,Cytoimmgen,17045 +26.03-test5,study,grouping,datasource,projectId,Fairfax_2014,16007 +26.03-test5,study,grouping,datasource,projectId,OneK1K,14362 +26.03-test5,study,grouping,datasource,projectId,CEDAR,14213 +26.03-test5,study,grouping,datasource,projectId,Bossini-Castillo_2019,12431 +26.03-test5,study,grouping,datasource,projectId,PhLiPS,10896 +26.03-test5,study,grouping,datasource,projectId,Walker_2019,10574 +26.03-test5,study,grouping,datasource,projectId,Nathan_2022,10540 +26.03-test5,study,grouping,datasource,projectId,Steinberg_2020,7678 +26.03-test5,study,grouping,datasource,projectId,iPSCORE,6582 +26.03-test5,study,grouping,datasource,projectId,Aygun_2021,6554 +26.03-test5,study,grouping,datasource,projectId,Schwartzentruber_2018,5339 +26.03-test5,study,grouping,datasource,projectId,van_de_Bunt_2015,5280 +26.03-test5,study,grouping,datasource,projectId,Kim-Hellmuth_2017,5254 +26.03-test5,study,grouping,datasource,projectId,PISA,5020 +26.03-test5,study,grouping,datasource,projectId,Jerber_2021,4869 +26.03-test5,study,grouping,datasource,projectId,Peng_2018,4775 +26.03-test5,study,grouping,datasource,projectId,Braineac2,3307 +26.03-test5,study,grouping,datasource,projectId,Kasela_2017,3194 +26.03-test5,study,grouping,datasource,projectId,Gilchrist_2021,3056 +26.03-test5,study,grouping,datasource,projectId,UKB_PPP_EUR,2953 +26.03-test5,study,grouping,datasource,projectId,Fairfax_2012,2900 +26.03-test5,study,grouping,datasource,projectId,FINNGEN_R12,2322 +26.03-test5,study,grouping,datasource,projectId,Perez_2022,1863 +26.03-test5,study,grouping,datasource,projectId,Young_2019,1793 +26.03-test5,study,grouping,datasource,projectId,Randolph_2021,1396 +26.03-test5,study,grouping,datasource,projectId,Naranbhai_2015,1117 +26.03-test5,study,grouping,datasource,projectId,Sun_2018,802 +26.03-test5,study,grouping,studyType,studyType,eqtl,1233604 +26.03-test5,study,grouping,studyType,studyType,tuqtl,364196 +26.03-test5,study,grouping,studyType,studyType,sqtl,213952 +26.03-test5,study,grouping,studyType,studyType,gwas,136245 +26.03-test5,study,grouping,studyType,studyType,sceqtl,50075 +26.03-test5,study,grouping,studyType,studyType,pqtl,3755 +26.03-test5,study,scalar,count,,,2001827 +26.03-test5,study,scalar,file_size,,,96590973 +26.03-test5,study,scalar,number_of_partitions,,,1 +26.03-test5,target,scalar,count,,,78691 +26.03-test5,target,scalar,file_size,,,85031914 +26.03-test5,target,scalar,number_of_partitions,,,10 +26.03-test5,target_essentiality,scalar,count,,,17844 +26.03-test5,target_essentiality,scalar,file_size,,,253322991 +26.03-test5,target_essentiality,scalar,number_of_partitions,,,10 +26.03-test5,target_prioritisation,scalar,count,,,78691 +26.03-test5,target_prioritisation,scalar,file_size,,,904101 +26.03-test5,target_prioritisation,scalar,number_of_partitions,,,1 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001627,3600043 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001631,1593255 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001583,1041076 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001819,424621 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001624,141974 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001589,115302 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001792,99257 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001630,94916 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0002169,76512 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001587,71274 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001575,45850 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001574,40606 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001623,23283 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0002170,22324 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001822,15238 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001787,12156 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001821,6410 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001632,3210 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0002012,2642 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001578,917 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001818,739 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001628,336 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001567,331 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001620,151 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001893,94 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,SO_0001580,31 +26.03-test5,variant,grouping,consequence,mostSevereConsequenceId,null,1 +26.03-test5,variant,scalar,count,,,7432549 +26.03-test5,variant,scalar,file_size,,,3421215184 +26.03-test5,variant,scalar,number_of_partitions,,,25 diff --git a/packages/ot-constants/src/index.ts b/packages/ot-constants/src/index.ts index 98cfcf85c..524ff8bd3 100644 --- a/packages/ot-constants/src/index.ts +++ b/packages/ot-constants/src/index.ts @@ -455,6 +455,76 @@ export const baselineUnits = { "mass-spectrometry proteomics": "PPB", }; +export const therapeuticPriorities = { + EFO_0001444: { name: "measurement", rank: 1 }, + MONDO_0045024: { name: "cancer or benign tumor", rank: 2 }, + OTAR_0000018: { name: "genetic, familial or congenital", rank: 3 }, + MONDO_0005550: { name: "infectious disease", rank: 4 }, + OTAR_0000009: { name: "injury, poisoning or complication", rank: 5 }, + OTAR_0000014: { name: "pregnancy or perinatal", rank: 6 }, + MONDO_0024458: { name: "visual system", rank: 7 }, + MONDO_0004995: { name: "cardiovascular", rank: 8 }, + MONDO_0002356: { name: "pancreas", rank: 9 }, + MONDO_0002515: { name: "liver", rank: 10 }, + EFO_0010282: { name: "gastrointestinal", rank: 11 }, + OTAR_0000017: { name: "reproductive system or breast", rank: 12 }, + MONDO_0002051: { name: "integumentary system", rank: 13 }, + MONDO_0005151: { name: "endocrine system", rank: 14 }, + OTAR_0000010: { name: "respiratory or thoracic", rank: 15 }, + MONDO_0002118: { name: "urinary system", rank: 16 }, + OTAR_0000006: { name: "musculoskeletal or connective ...", rank: 17 }, + MONDO_0021205: { name: "disorder of ear", rank: 18 }, + MONDO_0005046: { name: "immune system", rank: 19 }, + MONDO_0005570: { name: "hematologic", rank: 20 }, + MONDO_0005071: { name: "nervous system", rank: 21 }, + MONDO_0002025: { name: "psychiatric", rank: 22 }, + OTAR_0000020: { name: "nutritional or metabolic", rank: 23 }, + GO_0008150: { name: "biological process", rank: 24 }, + EFO_0000651: { name: "phenotype", rank: 25 }, + EFO_0002571: { name: "medical procedure", rank: 26 }, + MONDO_0005583: { name: "animal disease", rank: 27 }, +}; + +export const therapeuticAreas: Record = { + EFO_0001444: "measurement", + MONDO_0045024: "cancer or benign tumor", + OTAR_0000018: "genetic, familial or congenital", + MONDO_0005550: "infectious disease", + OTAR_0000009: "injury, poisoning or complication", + OTAR_0000014: "pregnancy or perinatal", + MONDO_0024458: "visual system", + MONDO_0004995: "cardiovascular", + MONDO_0002356: "pancreas", + MONDO_0002515: "liver", + EFO_0010282: "gastrointestinal", + OTAR_0000017: "reproductive system or breast", + MONDO_0002051: "integumentary system", + MONDO_0005151: "endocrine system", + OTAR_0000010: "respiratory or thoracic", + MONDO_0002118: "urinary system", + OTAR_0000006: "musculoskeletal or connective tissue", + MONDO_0021205: "disorder of ear", + MONDO_0005046: "immune system", + MONDO_0005570: "hematologic", + MONDO_0005071: "nervous system", + MONDO_0002025: "psychiatric", + OTAR_0000020: "nutritional or metabolic", + GO_0008150: "biological process", + EFO_0000651: "phenotype", + EFO_0002571: "medical procedure", + MONDO_0005583: "animal disease", + EFO_0000319: "cardiovascular", + EFO_0000540: "immune system", + EFO_0000618: "nervous system", + EFO_0001379: "endocrine system", + EFO_0005741: "infectious disease", + EFO_0005803: "hematologic", + EFO_0005932: "animal disease", + EFO_0009690: "urinary system", + EFO_0010284: "liver", + EFO_0010285: "integumentary system", +}; + export * from "./alphaFold"; export * from "./dataTypes"; export * from "./particlesBackground"; diff --git a/packages/sections/src/variant/GWASCredibleSets/PheWasPlot.tsx b/packages/sections/src/variant/GWASCredibleSets/PheWasPlot.tsx index 52f1a04a6..80206b902 100644 --- a/packages/sections/src/variant/GWASCredibleSets/PheWasPlot.tsx +++ b/packages/sections/src/variant/GWASCredibleSets/PheWasPlot.tsx @@ -1,6 +1,6 @@ import { Box, Chip, Skeleton, Typography, useTheme } from "@mui/material"; import * as PlotLib from "@observablehq/plot"; -import { credsetConfidenceMap, naLabel } from "@ot/constants"; +import { credsetConfidenceMap, naLabel, therapeuticPriorities } from "@ot/constants"; import { Fragment } from "react"; import { ClinvarStars, @@ -55,36 +55,6 @@ function PheWasPlot({ ); if (data.length === 0) return null; - const therapeuticPriorities = { - EFO_0001444: { name: "measurement", rank: 1 }, - MONDO_0045024: { name: "cancer or benign tumor", rank: 2 }, - OTAR_0000018: { name: "genetic, familial or congenital", rank: 3 }, - MONDO_0005550: { name: "infectious disease", rank: 4 }, - OTAR_0000009: { name: "injury, poisoning or complication", rank: 5 }, - OTAR_0000014: { name: "pregnancy or perinatal", rank: 6 }, - MONDO_0024458: { name: "visual system", rank: 7 }, - MONDO_0004995: { name: "cardiovascular", rank: 8 }, - MONDO_0002356: { name: "pancreas", rank: 9 }, - MONDO_0002515: { name: "liver", rank: 10 }, - EFO_0010282: { name: "gastrointestinal", rank: 11 }, - OTAR_0000017: { name: "reproductive system or breast", rank: 12 }, - MONDO_0002051: { name: "integumentary system", rank: 13 }, - MONDO_0005151: { name: "endocrine system", rank: 14 }, - OTAR_0000010: { name: "respiratory or thoracic", rank: 15 }, - MONDO_0002118: { name: "urinary system", rank: 16 }, - OTAR_0000006: { name: "musculoskeletal or connective ...", rank: 17 }, - MONDO_0021205: { name: "disorder of ear", rank: 18 }, - MONDO_0005046: { name: "immune system", rank: 19 }, - MONDO_0005570: { name: "hematologic", rank: 20 }, - MONDO_0005071: { name: "nervous system", rank: 21 }, - MONDO_0002025: { name: "psychiatric", rank: 22 }, - OTAR_0000020: { name: "nutritional or metabolic", rank: 23 }, - GO_0008150: { name: "biological process", rank: 24 }, - EFO_0000651: { name: "phenotype", rank: 25 }, - EFO_0002571: { name: "medical procedure", rank: 26 }, - MONDO_0005583: { name: "animal disease", rank: 27 }, - }; - function getTherapeuticArea(row) { let bestId = null; let bestRank = Infinity; From f3a9f4cd4ec04dc4c37df2ff3971e188ea5ea097 Mon Sep 17 00:00:00 2001 From: Graham McNeill Date: Thu, 20 Aug 2026 11:04:31 +0100 Subject: [PATCH 03/11] use PageMeta instead of BasePage --- .../src/pages/MetricsPage/MetricsPageWrapper.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/platform/src/pages/MetricsPage/MetricsPageWrapper.tsx b/apps/platform/src/pages/MetricsPage/MetricsPageWrapper.tsx index 925da6d54..12ede9bf1 100644 --- a/apps/platform/src/pages/MetricsPage/MetricsPageWrapper.tsx +++ b/apps/platform/src/pages/MetricsPage/MetricsPageWrapper.tsx @@ -1,15 +1,23 @@ import { Suspense, lazy } from "react"; -import { BasePage, LoadingBackdrop } from "ui"; +import { useLocation } from "react-router"; +import { LoadingBackdrop, PageMeta } from "ui"; const MetricsPage = lazy(() => import("./MetricsPage")); function MetricsPageWrapper() { + const location = useLocation(); + return ( - + <> + }> - + ); } From 7503fb7e3ef0b604e7b54d8babf5d0c1725110e3 Mon Sep 17 00:00:00 2001 From: Graham McNeill Date: Thu, 20 Aug 2026 16:42:32 +0100 Subject: [PATCH 04/11] refactor and polish appearance --- .../MetricsPage/DiseasesByTherapeuticArea.tsx | 174 +++++++++++++++ .../src/pages/MetricsPage/MetricsCards.tsx | 76 +++++++ .../src/pages/MetricsPage/MetricsPage.tsx | 203 +----------------- 3 files changed, 259 insertions(+), 194 deletions(-) create mode 100644 apps/platform/src/pages/MetricsPage/DiseasesByTherapeuticArea.tsx create mode 100644 apps/platform/src/pages/MetricsPage/MetricsCards.tsx diff --git a/apps/platform/src/pages/MetricsPage/DiseasesByTherapeuticArea.tsx b/apps/platform/src/pages/MetricsPage/DiseasesByTherapeuticArea.tsx new file mode 100644 index 000000000..500d64aaa --- /dev/null +++ b/apps/platform/src/pages/MetricsPage/DiseasesByTherapeuticArea.tsx @@ -0,0 +1,174 @@ +import { Box, Typography, Paper, useTheme } from "@mui/material"; +import * as Plot from "@observablehq/plot"; +import { therapeuticAreas } from "@ot/constants"; +import { ObsPlot } from "ui"; +import type { MetricRow } from "./MetricsPage"; + +type TherapeuticAreaCount = { + name: string; + count: number; + areas?: TherapeuticAreaCount[]; +}; + +const count = (rows: MetricRow[], dataset: string) => + rows.find( + (row) => + row.dataset === dataset && + row.kind === "scalar" && + row.metric === "count", + )?.value ?? 0; + +function DiseasesByTherapeuticArea({ data }: { data: MetricRow[] }) { + const theme = useTheme(); + console.log(theme) + const areas = data + .filter( + (row) => + row.dataset === "disease" && + row.kind === "grouping" && + row.metric === "therapeuticArea", + ) + .map((row) => ({ + name: therapeuticAreas[row.group_value] ?? row.group_value, + count: row.value, + })); + const total = count(data, "disease"); + const otherAreas = areas + .filter((area) => area.count < total * 0.02) + .sort((a, b) => b.count - a.count); + const chartData: TherapeuticAreaCount[] = areas + .filter((area) => area.count >= total * 0.02) + .sort((a, b) => b.count - a.count); + + if (otherAreas.length > 0) { + chartData.push({ + name: "Other", + count: otherAreas.reduce((sum, area) => sum + area.count, 0), + areas: otherAreas, + }); + } + + if (chartData.length === 0) return null; + + return ( + + + + Diseases by therapeutic area + + area.count} + yTooltip={(area) => area.name} + xAnchorTooltip="adapt" + yAnchorTooltip="adapt" + renderTooltip={renderTooltip} + gapInfo={0} + renderInfo={() => null} + /> + + + ); + + function renderChart({ + data, + otherData, + width, + height, + }: { + data: TherapeuticAreaCount[]; + otherData?: { textColor: string }; + width?: number; + height: number; + }) { + const plotWidth = Math.max((width ?? 0) - 210 - 24, 0); + const maxCount = Math.max(...data.map((area) => area.count)); + const countLabelWidth = (area: TherapeuticAreaCount) => + `${area.count.toLocaleString()}`.length * 7 + 12; + const insideData = data.filter( + (area) => (area.count / maxCount) * plotWidth >= countLabelWidth(area), + ); + const outsideData = data.filter((area) => !insideData.includes(area)); + + return Plot.plot({ + width: width ?? 0, + height, + style: { fontSize: "13.5px" }, + marginTop: 4, + marginBottom: 4, + marginLeft: 240, + marginRight: 0, + x: { axis: null }, + y: { + domain: data.map((area) => area.name), + label: null, + tickSize: 0, + tickPadding: 8, + tickFormat: (name) => name, + }, + marks: [ + Plot.barX(data, { + x: "count", + y: "name", + // fill: (area) => (area.areas ? "#5b89b0" : "#1963a3"), + fill: (area) => (area.areas ? theme.palette.primary.light : theme.palette.primary.main), + insetTop: 2, + insetBottom: 2, + className: "obs-tooltip", + }), + Plot.text(insideData, { + x: (area) => area.count, + y: "name", + text: (area) => area.count.toLocaleString(), + textAnchor: "end", + dx: -6, + fill: "white", + lineAnchor: "middle", + fontSize: 12.5, + className: "obs-tooltip", + }), + Plot.text(outsideData, { + x: (area) => area.count, + y: "name", + text: (area) => area.count.toLocaleString(), + textAnchor: "start", + dx: 6, + fill: otherData?.textColor ?? "currentColor", + lineAnchor: "middle", + fontSize: 12.5, + className: "obs-tooltip", + }), + ], + }); + } + +} + +function renderTooltip(area: TherapeuticAreaCount) { + if (!area.areas) return null; + + return ( + + {area.areas.map((otherArea) => ( + + {otherArea.name}: {otherArea.count.toLocaleString()} + + ))} + + ); +} + +export default DiseasesByTherapeuticArea; diff --git a/apps/platform/src/pages/MetricsPage/MetricsCards.tsx b/apps/platform/src/pages/MetricsPage/MetricsCards.tsx new file mode 100644 index 000000000..18b5c19ba --- /dev/null +++ b/apps/platform/src/pages/MetricsPage/MetricsCards.tsx @@ -0,0 +1,76 @@ +import { Box, Card, CardContent, Typography } from "@mui/material"; +import { format } from "d3"; +import { + faChartBar, + faDna, + faMapPin, + faPrescriptionBottleMedical, + faStethoscope, + faProjectDiagram, + faCircleNodes +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; +import { Tooltip } from "ui"; +import type { MetricRow } from "./MetricsPage"; + +const count = (rows: MetricRow[], dataset: string) => + rows.find((row) => row.dataset === dataset && row.kind === "scalar" && row.metric === "count")?.value ?? 0; + +const formatRoundedCount = (value: number) => { + if (value === 0) return "0"; + + const unitExponent = Math.max(0, Math.floor(Math.log10(Math.abs(value)) / 3)); + const unit = 10 ** (unitExponent * 3); + const roundedValue = Math.round((value / unit) * 10) / 10; + return format("~s")(roundedValue * unit); +}; + +function MetricsCards({ data }: { data: MetricRow[] }) { + const metrics = [ + ["Targets", faDna, count(data, "target")], + ["Diseases", faStethoscope, count(data, "disease")], + ["Drugs", faPrescriptionBottleMedical, count(data, "drug_molecule")], + ["Studies", faChartBar, count(data, "study")], + ["Credible sets", faProjectDiagram, count(data, "credible_set")], + ["Evidence", faProjectDiagram, data.filter((row) => row.dataset.startsWith("evidence_") && row.metric === "count").reduce((sum, row) => sum + row.value, 0)], + ["Variants", faMapPin, count(data, "variant")], + ["Prioritised genes", faDna, data.find((row) => row.dataset === "l2g_prediction" && row.kind === "filter" && row.metric === "prioritised_genes")?.value ?? 0], + ["GWAS/GWAS colocs", faCircleNodes, data.find((row) => row.dataset === "colocalisation" && row.group_value === "gwas-gwas")?.value ?? 0], + ["GWAS/QTL colocs", faCircleNodes, data.find((row) => row.dataset === "colocalisation" && row.group_value === "gwas-eqtl")?.value ?? 0], + ] as const; + + return ( + + {metrics.map(([label, icon, value]) => ( + + + + + + + {label} + + {formatRoundedCount(value)} + + + + + ))} + + ); +} + +export default MetricsCards; diff --git a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx index 80b726df3..6ef8e72d8 100644 --- a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx +++ b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx @@ -1,213 +1,28 @@ import { useEffect, useState } from "react"; import { autoType, csv } from "d3"; -import { Box, Card, CardContent, Grid, Typography, useTheme } from "@mui/material"; -import { format } from "d3"; -import * as Plot from "@observablehq/plot"; -import { therapeuticAreas } from "@ot/constants"; -import { ObsPlot } from "ui"; +import { Typography } from "@mui/material"; import metricsCsv from "./metrics.csv?url"; -import { - faBook, - faChartBar, - faDna, - faLink, - faMapPin, - faNetworkWired, - faPrescriptionBottleMedical, - faStethoscope, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; +import MetricsCards from "./MetricsCards"; +import DiseasesByTherapeuticArea from "./DiseasesByTherapeuticArea"; -type MetricRow = { dataset: string; kind: string; metric: string; group_value: string; value: number }; -type TherapeuticAreaCount = { - name: string; - count: number; - areas?: { name: string; count: number }[]; -}; - -const count = (rows: MetricRow[], dataset: string) => - rows.find((row) => row.dataset === dataset && row.kind === "scalar" && row.metric === "count")?.value ?? 0; - -function DiseaseByTherapeuticArea({ data }: { data: MetricRow[] }) { - const theme = useTheme(); - const areas = data - .filter((row) => row.dataset === "disease" && row.kind === "grouping" && row.metric === "therapeuticArea") - .map((row) => ({ - name: therapeuticAreas[row.group_value] ?? row.group_value, - count: row.value, - })); - const total = count(data, "disease"); - const otherAreas = areas.filter((area) => area.count < total * 0.02).sort((a, b) => b.count - a.count); - const chartData: TherapeuticAreaCount[] = areas - .filter((area) => area.count >= total * 0.02) - .sort((a, b) => b.count - a.count); - - if (otherAreas.length > 0) { - chartData.push({ - name: "Other", - count: otherAreas.reduce((sum, area) => sum + area.count, 0), - areas: otherAreas, - }); - } - - if (chartData.length === 0) return null; - - return ( - - - Diseases by therapeutic area - - area.count} - yTooltip={(area) => area.name} - xAnchorTooltip="adapt" - yAnchorTooltip="adapt" - renderTooltip={renderDiseaseByTherapeuticAreaTooltip} - gapInfo={0} - renderInfo={() => null} - /> - - ); -} - -function renderDiseaseByTherapeuticAreaChart({ - data, - otherData, - width, - height, -}: { - data: TherapeuticAreaCount[]; - otherData?: { textColor: string }; - width?: number; - height: number; -}) { - const plotWidth = Math.max((width ?? 0) - 210 - 24, 0); - const maxCount = Math.max(...data.map((area) => area.count)); - const countLabelWidth = (area: TherapeuticAreaCount) => `${area.count.toLocaleString()}`.length * 7 + 12; - const isInside = (area: TherapeuticAreaCount) => (area.count / maxCount) * plotWidth >= countLabelWidth(area); - const insideData = data.filter(isInside); - const outsideData = data.filter((area) => !isInside(area)); - - return Plot.plot({ - width: width ?? 0, - height, - style: { fontSize: "14px" }, - marginTop: 4, - marginBottom: 4, - marginLeft: 210, - marginRight: 24, - x: { axis: null }, - y: { domain: data.map((area) => area.name), label: null, tickSize: 0, tickPadding: 8, tickFormat: (name) => name }, - marks: [ - Plot.barX(data, { - x: "count", - y: "name", - fill: (area) => (area.areas ? "#5b89b0" : "#1963a3"), - insetTop: 3, - insetBottom: 3, - className: "obs-tooltip", - }), - Plot.text(insideData, { - x: (area) => area.count, - y: "name", - text: (area) => area.count.toLocaleString(), - textAnchor: "end", - dx: -6, - fill: "white", - lineAnchor: "middle", - fontSize: 13, - className: "obs-tooltip", - }), - Plot.text(outsideData, { - x: (area) => area.count, - y: "name", - text: (area) => area.count.toLocaleString(), - textAnchor: "start", - dx: 6, - fill: otherData?.textColor ?? "currentColor", - lineAnchor: "middle", - fontSize: 13, - className: "obs-tooltip", - }), - ], - }); -} - -function renderDiseaseByTherapeuticAreaTooltip(area: TherapeuticAreaCount) { - if (!area.areas) return null; - - return ( - - {area.areas?.map((otherArea) => ( - - {otherArea.name}: {otherArea.count.toLocaleString()} - - ))} - - ); -} +export type MetricRow = { dataset: string; kind: string; metric: string; group_value: string; value: number }; function MetricsPage() { const [data, setData] = useState([]); useEffect(() => { - csv(metricsCsv, autoType).then((d) => { - setData(d as unknown as MetricRow[]); - }); + csv(metricsCsv, autoType).then((d) => setData(d as unknown as MetricRow[])); }, []); return ( <> Data Metrics - - - {[ - ["Targets", faDna, count(data, "target")], - ["Diseases", faStethoscope, count(data, "disease")], - ["Drugs", faPrescriptionBottleMedical, count(data, "drug_molecule")], - ["Studies", faChartBar, count(data, "study")], - ["Credible sets", faBook, count(data, "credible_set")], - ["Evidence", faLink, data.filter((row) => row.dataset.startsWith("evidence_") && row.metric === "count").reduce((sum, row) => sum + row.value, 0)], - ["Variants", faMapPin, count(data, "variant")], - ["Prioritised genes", faDna, data.find((row) => row.dataset === "l2g_prediction" && row.kind === "filter" && row.metric === "prioritised_genes")?.value ?? 0], - ["GWAS/GWAS colocs", faNetworkWired, data.find((row) => row.dataset === "colocalisation" && row.group_value === "gwas-gwas")?.value ?? 0], - ["GWAS/QTL colocs", faNetworkWired, data.find((row) => row.dataset === "colocalisation" && row.group_value === "gwas-eqtl")?.value ?? 0], - ].map(([label, icon, value]) => ( - - - - - {label} - {format("~s")(value as number)} - - - - ))} - - Polish: capitalise titles, round numbers, use correct icons, make less like buttons(?) + + Todo: finalise card order and icons - what for coloc? evidence and cred sets ok to be same? Alternative: more hierarchical, e.g. split into top-level entity counts then evidence linking targets and diseases, credible sets and colocs in variants section. - Coverage - - Polish: bars are so 'strong' - maybe ok if will be 1/2 page otherwise consider outline bars - Alternative:Replace Other with 'show more'? - - + + Alternative:Replace Other with 'show more'? ); } From fb677b7e8adc17c9100a52cadad56806bc21d232 Mon Sep 17 00:00:00 2001 From: Graham McNeill Date: Fri, 21 Aug 2026 15:23:16 +0100 Subject: [PATCH 05/11] add remaining plots --- .../MetricsPage/CredibleSetsByStudyType.tsx | 124 +++++++++++++ .../MetricsPage/DiseasesByTherapeuticArea.tsx | 5 + .../MetricsPage/DrugsByClinicalStage.tsx | 125 +++++++++++++ .../pages/MetricsPage/EvidenceByDataType.tsx | 126 +++++++++++++ .../src/pages/MetricsPage/MetricsPage.tsx | 21 ++- .../MetricsPage/VariantsByConsequence.tsx | 166 +++++++++++++++++ packages/ot-constants/src/index.ts | 170 ++++++++++++++++++ 7 files changed, 735 insertions(+), 2 deletions(-) create mode 100644 apps/platform/src/pages/MetricsPage/CredibleSetsByStudyType.tsx create mode 100644 apps/platform/src/pages/MetricsPage/DrugsByClinicalStage.tsx create mode 100644 apps/platform/src/pages/MetricsPage/EvidenceByDataType.tsx create mode 100644 apps/platform/src/pages/MetricsPage/VariantsByConsequence.tsx diff --git a/apps/platform/src/pages/MetricsPage/CredibleSetsByStudyType.tsx b/apps/platform/src/pages/MetricsPage/CredibleSetsByStudyType.tsx new file mode 100644 index 000000000..2cf95a6a4 --- /dev/null +++ b/apps/platform/src/pages/MetricsPage/CredibleSetsByStudyType.tsx @@ -0,0 +1,124 @@ +import { Box, Paper, Typography, useTheme } from "@mui/material"; +import * as Plot from "@observablehq/plot"; +import { ObsPlot } from "ui"; +import type { MetricRow } from "./MetricsPage"; + +type StudyTypeCount = { name: string; count: number }; + +function CredibleSetsByStudyType({ data }: { data: MetricRow[] }) { + const theme = useTheme(); + const chartData: StudyTypeCount[] = data + .filter( + (row) => + row.dataset === "credible_set" && + row.kind === "grouping" && + row.expression === "studyType" && + row.group_value, + ) + .map((row) => ({ name: row.group_value, count: row.value })) + .sort((a, b) => b.count - a.count); + + if (chartData.length === 0) return null; + + return ( + + + + Credible sets by study type + + item.count} + yTooltip={(item) => item.name} + xAnchorTooltip="adapt" + yAnchorTooltip="adapt" + gapInfo={0} + renderInfo={() => null} + /> + + + ); + + function renderChart({ + data, + width, + height, + }: { + data: StudyTypeCount[]; + width?: number; + height: number; + }) { + const plotWidth = Math.max((width ?? 0) - 210 - 24, 0); + const maxCount = Math.max(...data.map((item) => item.count)); + const insideData = data.filter( + (item) => + (item.count / maxCount) * plotWidth >= + `${item.count.toLocaleString()}`.length * 7 + 12, + ); + const outsideData = data.filter((item) => !insideData.includes(item)); + + return Plot.plot({ + width: width ?? 0, + height, + style: { fontSize: "13.5px" }, + marginTop: 4, + marginBottom: 4, + marginLeft: 240, + marginRight: 0, + x: { axis: null }, + y: { + domain: data.map((item) => item.name), + label: null, + tickSize: 0, + tickPadding: 8, + tickFormat: label => { + return label.endsWith('qtl') + ? `${label.slice(0, -3)}QTL` + : label.toUpperCase() + } + }, + marks: [ + Plot.barX(data, { + x: "count", + y: "name", + fill: theme.palette.primary.main, + insetTop: 2, + insetBottom: 2, + className: "obs-tooltip", + }), + Plot.text(insideData, { + x: "count", + y: "name", + text: (item) => item.count.toLocaleString(), + textAnchor: "end", + dx: -6, + fill: "white", + lineAnchor: "middle", + fontSize: 12.5, + className: "obs-tooltip", + }), + Plot.text(outsideData, { + x: "count", + y: "name", + text: (item) => item.count.toLocaleString(), + textAnchor: "start", + dx: 6, + fill: theme.palette.text.primary, + lineAnchor: "middle", + fontSize: 12.5, + className: "obs-tooltip", + }), + ], + }); + } +} + +export default CredibleSetsByStudyType; diff --git a/apps/platform/src/pages/MetricsPage/DiseasesByTherapeuticArea.tsx b/apps/platform/src/pages/MetricsPage/DiseasesByTherapeuticArea.tsx index 500d64aaa..a7b393ffd 100644 --- a/apps/platform/src/pages/MetricsPage/DiseasesByTherapeuticArea.tsx +++ b/apps/platform/src/pages/MetricsPage/DiseasesByTherapeuticArea.tsx @@ -71,6 +71,9 @@ function DiseasesByTherapeuticArea({ data }: { data: MetricRow[] }) { renderInfo={() => null} /> + + A disease can belong to more than one therapeutic area + ); @@ -129,6 +132,7 @@ function DiseasesByTherapeuticArea({ data }: { data: MetricRow[] }) { fill: "white", lineAnchor: "middle", fontSize: 12.5, + pointerEvents: "none", className: "obs-tooltip", }), Plot.text(outsideData, { @@ -140,6 +144,7 @@ function DiseasesByTherapeuticArea({ data }: { data: MetricRow[] }) { fill: otherData?.textColor ?? "currentColor", lineAnchor: "middle", fontSize: 12.5, + pointerEvents: "none", className: "obs-tooltip", }), ], diff --git a/apps/platform/src/pages/MetricsPage/DrugsByClinicalStage.tsx b/apps/platform/src/pages/MetricsPage/DrugsByClinicalStage.tsx new file mode 100644 index 000000000..77f12d3ee --- /dev/null +++ b/apps/platform/src/pages/MetricsPage/DrugsByClinicalStage.tsx @@ -0,0 +1,125 @@ +import { Box, Paper, Typography, useTheme } from "@mui/material"; +import * as Plot from "@observablehq/plot"; +import { clinicalStageCategories } from "@ot/constants"; +import { ObsPlot } from "ui"; +import type { MetricRow } from "./MetricsPage"; + +type ClinicalStageCount = { name: string; count: number }; + +function DrugsByClinicalStage({ data }: { data: MetricRow[] }) { + const theme = useTheme(); + const chartData: ClinicalStageCount[] = data + .filter( + (row) => + row.dataset === "drug_molecule" && + row.kind === "grouping" && + row.metric === "clinicalStage" && + row.group_value, + ) + .map((row) => ({ + name: + clinicalStageCategories[row.group_value as keyof typeof clinicalStageCategories]?.label ?? + row.group_value, + count: row.value, + })) + .sort((a, b) => b.count - a.count); + + if (chartData.length === 0) return null; + + return ( + + + + Drugs by clinical stage + + item.count} + yTooltip={(item) => item.name} + xAnchorTooltip="adapt" + yAnchorTooltip="adapt" + gapInfo={0} + renderInfo={() => null} + /> + + + ); + + function renderChart({ + data, + width, + height, + }: { + data: ClinicalStageCount[]; + width?: number; + height: number; + }) { + const plotWidth = Math.max((width ?? 0) - 210 - 24, 0); + const maxCount = Math.max(...data.map((item) => item.count)); + const insideData = data.filter( + (item) => + (item.count / maxCount) * plotWidth >= + `${item.count.toLocaleString()}`.length * 7 + 12, + ); + const outsideData = data.filter((item) => !insideData.includes(item)); + + return Plot.plot({ + width: width ?? 0, + height, + style: { fontSize: "13.5px" }, + marginTop: 4, + marginBottom: 4, + marginLeft: 240, + marginRight: 0, + x: { axis: null }, + y: { + domain: data.map((item) => item.name), + label: null, + tickSize: 0, + tickPadding: 8, + }, + marks: [ + Plot.barX(data, { + x: "count", + y: "name", + fill: theme.palette.primary.main, + insetTop: 2, + insetBottom: 2, + className: "obs-tooltip", + }), + Plot.text(insideData, { + x: "count", + y: "name", + text: (item) => item.count.toLocaleString(), + textAnchor: "end", + dx: -6, + fill: "white", + lineAnchor: "middle", + fontSize: 12.5, + className: "obs-tooltip", + }), + Plot.text(outsideData, { + x: "count", + y: "name", + text: (item) => item.count.toLocaleString(), + textAnchor: "start", + dx: 6, + fill: theme.palette.text.primary, + lineAnchor: "middle", + fontSize: 12.5, + className: "obs-tooltip", + }), + ], + }); + } +} + +export default DrugsByClinicalStage; diff --git a/apps/platform/src/pages/MetricsPage/EvidenceByDataType.tsx b/apps/platform/src/pages/MetricsPage/EvidenceByDataType.tsx new file mode 100644 index 000000000..719514804 --- /dev/null +++ b/apps/platform/src/pages/MetricsPage/EvidenceByDataType.tsx @@ -0,0 +1,126 @@ +import { Box, Paper, Typography, useTheme } from "@mui/material"; +import * as Plot from "@observablehq/plot"; +import { ObsPlot } from "ui"; +import type { MetricRow } from "./MetricsPage"; + +type DataTypeCount = { name: string; count: number }; + +function EvidenceByDataType({ data }: { data: MetricRow[] }) { + const theme = useTheme(); + const counts = new Map(); + + data + .filter( + (row) => + row.dataset.startsWith("evidence_") && + row.kind === "grouping" && + row.group_value, + ) + .forEach((row) => + counts.set(row.group_value, (counts.get(row.group_value) ?? 0) + row.value), + ); + + const chartData: DataTypeCount[] = [...counts] + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count); + + if (chartData.length === 0) return null; + + return ( + + + + Evidence by data type + + item.count} + yTooltip={(item) => item.name} + xAnchorTooltip="adapt" + yAnchorTooltip="adapt" + gapInfo={0} + renderInfo={() => null} + /> + + + ); + + function renderChart({ + data, + width, + height, + }: { + data: DataTypeCount[]; + width?: number; + height: number; + }) { + const plotWidth = Math.max((width ?? 0) - 210 - 24, 0); + const maxCount = Math.max(...data.map((item) => item.count)); + const insideData = data.filter( + (item) => + (item.count / maxCount) * plotWidth >= + `${item.count.toLocaleString()}`.length * 7 + 12, + ); + const outsideData = data.filter((item) => !insideData.includes(item)); + + return Plot.plot({ + width: width ?? 0, + height, + style: { fontSize: "13.5px" }, + marginTop: 4, + marginBottom: 4, + marginLeft: 240, + marginRight: 0, + x: { axis: null }, + y: { + domain: data.map((item) => item.name), + label: null, + tickSize: 0, + tickPadding: 8, + tickFormat: (name) => name.replaceAll("_", " "), + }, + marks: [ + Plot.barX(data, { + x: "count", + y: "name", + fill: theme.palette.primary.main, + insetTop: 2, + insetBottom: 2, + className: "obs-tooltip", + }), + Plot.text(insideData, { + x: "count", + y: "name", + text: (item) => item.count.toLocaleString(), + textAnchor: "end", + dx: -6, + fill: "white", + lineAnchor: "middle", + fontSize: 12.5, + className: "obs-tooltip", + }), + Plot.text(outsideData, { + x: "count", + y: "name", + text: (item) => item.count.toLocaleString(), + textAnchor: "start", + dx: 6, + fill: theme.palette.text.primary, + lineAnchor: "middle", + fontSize: 12.5, + className: "obs-tooltip", + }), + ], + }); + } +} + +export default EvidenceByDataType; diff --git a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx index 6ef8e72d8..d65e952d0 100644 --- a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx +++ b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx @@ -4,6 +4,10 @@ import { Typography } from "@mui/material"; import metricsCsv from "./metrics.csv?url"; import MetricsCards from "./MetricsCards"; import DiseasesByTherapeuticArea from "./DiseasesByTherapeuticArea"; +import EvidenceByDataType from "./EvidenceByDataType"; +import DrugsByClinicalStage from "./DrugsByClinicalStage"; +import CredibleSetsByStudyType from "./CredibleSetsByStudyType"; +import VariantsByConsequence from "./VariantsByConsequence"; export type MetricRow = { dataset: string; kind: string; metric: string; group_value: string; value: number }; @@ -18,11 +22,24 @@ function MetricsPage() { <> Data Metrics - Todo: finalise card order and icons - what for coloc? evidence and cred sets ok to be same? + Polish: do these look like buttons? + Todo: finalise card order and icons - what for coloc? evidence and cred sets ok to be same? more info in tooltip where approp? - e.g. explain a prioritised gene Alternative: more hierarchical, e.g. split into top-level entity counts then evidence linking targets and diseases, credible sets and colocs in variants section. + Coverage - Alternative:Replace Other with 'show more'? + Alternative: Replace Other+tooltip with 'show more'? + + Alternative: + + Alternative: + + Genetics + + Alternative: + + Polish:Can we remove "variant" from every bar label? + Alternative:Replace Other+tooltip with 'show more'? ); } diff --git a/apps/platform/src/pages/MetricsPage/VariantsByConsequence.tsx b/apps/platform/src/pages/MetricsPage/VariantsByConsequence.tsx new file mode 100644 index 000000000..7ccc733cd --- /dev/null +++ b/apps/platform/src/pages/MetricsPage/VariantsByConsequence.tsx @@ -0,0 +1,166 @@ +import { Box, Paper, Typography, useTheme } from "@mui/material"; +import * as Plot from "@observablehq/plot"; +import { PREDICTED_CONSEQUENCE_LOOKUP } from "@ot/constants"; +import { ObsPlot } from "ui"; +import type { MetricRow } from "./MetricsPage"; + +type ConsequenceCount = { name: string; count: number; consequences?: ConsequenceCount[] }; + +function VariantsByConsequence({ data }: { data: MetricRow[] }) { + const theme = useTheme(); + const consequences: ConsequenceCount[] = data + .filter( + (row) => + row.dataset === "variant" && + row.kind === "grouping" && + row.expression === "mostSevereConsequenceId" && + row.group_value, + ) + .map((row) => ({ + name: + PREDICTED_CONSEQUENCE_LOOKUP[ + row.group_value.replace("_", ":") as keyof typeof PREDICTED_CONSEQUENCE_LOOKUP + ]?.displayTerm ?? row.group_value, + count: row.value, + })) + .sort((a, b) => b.count - a.count); + const total = consequences.reduce((sum, consequence) => sum + consequence.count, 0); + const otherConsequences = consequences.filter( + (consequence) => consequence.count < total * 0.01, + ); + const chartData = consequences.filter((consequence) => consequence.count >= total * 0.01); + + if (otherConsequences.length > 0) { + chartData.push({ + name: "Other", + count: otherConsequences.reduce((sum, consequence) => sum + consequence.count, 0), + consequences: otherConsequences, + }); + } + + if (chartData.length === 0) return null; + + return ( + + + + Variants by most severe consequence + + item.count} + yTooltip={(item) => item.name} + xAnchorTooltip="adapt" + yAnchorTooltip="adapt" + renderTooltip={renderTooltip} + gapInfo={0} + renderInfo={() => null} + /> + + + ); + + function renderChart({ + data, + width, + height, + }: { + data: ConsequenceCount[]; + width?: number; + height: number; + }) { + const plotWidth = Math.max((width ?? 0) - 210 - 24, 0); + const maxCount = Math.max(...data.map((item) => item.count)); + const insideData = data.filter( + (item) => + (item.count / maxCount) * plotWidth >= + `${item.count.toLocaleString()}`.length * 7 + 12, + ); + const outsideData = data.filter((item) => !insideData.includes(item)); + + return Plot.plot({ + width: width ?? 0, + height, + style: { fontSize: "13.5px" }, + marginTop: 4, + marginBottom: 4, + marginLeft: 240, + marginRight: 0, + x: { axis: null }, + y: { + domain: data.map((item) => item.name), + label: null, + tickSize: 0, + tickPadding: 8, + }, + marks: [ + Plot.barX(data, { + x: "count", + y: "name", + fill: (item) => + item.consequences ? theme.palette.primary.light : theme.palette.primary.main, + insetTop: 2, + insetBottom: 2, + className: "obs-tooltip", + }), + Plot.text(insideData, { + x: (item) => item.count, + y: "name", + text: (item) => item.count.toLocaleString(), + textAnchor: "end", + dx: -6, + fill: "white", + lineAnchor: "middle", + fontSize: 12.5, + pointerEvents: "none", + className: "obs-tooltip", + }), + Plot.text(outsideData, { + x: (item) => item.count, + y: "name", + text: (item) => item.count.toLocaleString(), + textAnchor: "start", + dx: 6, + fill: theme.palette.text.primary, + lineAnchor: "middle", + fontSize: 12.5, + pointerEvents: "none", + className: "obs-tooltip", + }), + ], + }); + } +} + +function renderTooltip(consequence: ConsequenceCount) { + if (!consequence.consequences) return null; + + return ( + + {consequence.consequences.map((item) => ( + + {item.name}: {item.count.toLocaleString()} + + ))} + + ); +} + +export default VariantsByConsequence; diff --git a/packages/ot-constants/src/index.ts b/packages/ot-constants/src/index.ts index 524ff8bd3..1c313c64e 100644 --- a/packages/ot-constants/src/index.ts +++ b/packages/ot-constants/src/index.ts @@ -397,6 +397,176 @@ export const variantConsequenceSource = { }, }; +// from Ensembl: https://www.ensembl.org/info/genome/variation/prediction/predicted_data.html +export const PREDICTED_CONSEQUENCE_LOOKUP = { + "SO:0001893": { color: "#ff0000", displayTerm: "Transcript ablation", impact: "HIGH", rank: 0 }, + "SO:0001574": { + color: "#ff581a", + displayTerm: "Splice acceptor variant", + impact: "HIGH", + rank: 1, + }, + "SO:0001575": { color: "#ff581a", displayTerm: "Splice donor variant", impact: "HIGH", rank: 2 }, + "SO:0001587": { color: "#ff0000", displayTerm: "Stop gained", impact: "HIGH", rank: 3 }, + "SO:0001589": { color: "#9400d3", displayTerm: "Frameshift variant", impact: "HIGH", rank: 4 }, + "SO:0001578": { color: "#ff0000", displayTerm: "Stop lost", impact: "HIGH", rank: 5 }, + "SO:0002012": { color: "#ffd700", displayTerm: "Start lost", impact: "HIGH", rank: 6 }, + "SO:0001889": { + color: "#ff69b4", + displayTerm: "Transcript amplification", + impact: "HIGH", + rank: 7, + }, + "SO:0001907": { color: "#7f7f7f", displayTerm: "Feature elongation", impact: "HIGH", rank: 8 }, + "SO:0001906": { color: "#7f7f7f", displayTerm: "Feature truncation", impact: "HIGH", rank: 9 }, + "SO:0001821": { + color: "#ff69b4", + displayTerm: "Inframe insertion", + impact: "MODERATE", + rank: 10, + }, + "SO:0001822": { color: "#ff69b4", displayTerm: "Inframe deletion", impact: "MODERATE", rank: 11 }, + "SO:0001583": { color: "#ffd700", displayTerm: "Missense variant", impact: "MODERATE", rank: 12 }, + "SO:0001818": { + color: "#ff0080", + displayTerm: "Protein altering variant", + impact: "MODERATE", + rank: 13, + }, + "SO:0001787": { + color: "#ff7f50", + displayTerm: "Splice donor 5th base variant", + impact: "LOW", + rank: 14, + }, + "SO:0001630": { color: "#ff7f50", displayTerm: "Splice region variant", impact: "LOW", rank: 15 }, + "SO:0002170": { + color: "#ff7f50", + displayTerm: "Splice donor region variant", + impact: "LOW", + rank: 16, + }, + "SO:0002169": { + color: "#ff7f50", + displayTerm: "Splice polypyrimidine tract variant", + impact: "LOW", + rank: 17, + }, + "SO:0001626": { + color: "#ff00ff", + displayTerm: "Incomplete terminal codon variant", + impact: "LOW", + rank: 18, + }, + "SO:0002019": { + color: "#76ee00", + displayTerm: "Start retained variant", + impact: "LOW", + rank: 19, + }, + "SO:0001567": { color: "#76ee00", displayTerm: "Stop retained variant", impact: "LOW", rank: 20 }, + "SO:0001819": { color: "#76ee00", displayTerm: "Synonymous variant", impact: "LOW", rank: 21 }, + "SO:0001580": { + color: "#458b00", + displayTerm: "Coding sequence variant", + impact: "MODIFIER", + rank: 22, + }, + "SO:0001620": { + color: "#458b00", + displayTerm: "Mature miRNA variant", + impact: "MODIFIER", + rank: 23, + }, + "SO:0001623": { + color: "#7ac5cd", + displayTerm: "5 prime UTR variant", + impact: "MODIFIER", + rank: 24, + }, + "SO:0001624": { + color: "#7ac5cd", + displayTerm: "3 prime UTR variant", + impact: "MODIFIER", + rank: 25, + }, + "SO:0001792": { + color: "#32cd32", + displayTerm: "Non coding transcript exon variant", + impact: "MODIFIER", + rank: 26, + }, + "SO:0001627": { color: "#02599c", displayTerm: "Intron variant", impact: "MODIFIER", rank: 27 }, + "SO:0001621": { + color: "#ff4500", + displayTerm: "NMD transcript variant", + impact: "MODIFIER", + rank: 28, + }, + "SO:0001619": { + color: "#32cd32", + displayTerm: "Non coding transcript variant", + impact: "MODIFIER", + rank: 29, + }, + "SO:0001968": { + color: "#458b00", + displayTerm: "Coding transcript variant", + impact: "MODIFIER", + rank: 30, + }, + "SO:0001631": { + color: "#a2b5cd", + displayTerm: "Upstream gene variant", + impact: "MODIFIER", + rank: 31, + }, + "SO:0001632": { + color: "#a2b5cd", + displayTerm: "Downstream gene variant", + impact: "MODIFIER", + rank: 32, + }, + "SO:0001895": { color: "#a52a2a", displayTerm: "TFBS ablation", impact: "MODIFIER", rank: 33 }, + "SO:0001892": { + color: "#a52a2a", + displayTerm: "TFBS amplification", + impact: "MODIFIER", + rank: 34, + }, + "SO:0001782": { + color: "#a52a2a", + displayTerm: "TF binding site variant", + impact: "MODIFIER", + rank: 35, + }, + "SO:0001894": { + color: "#a52a2a", + displayTerm: "Regulatory region ablation", + impact: "MODIFIER", + rank: 36, + }, + "SO:0001891": { + color: "#a52a2a", + displayTerm: "Regulatory region amplification", + impact: "MODIFIER", + rank: 37, + }, + "SO:0001566": { + color: "#a52a2a", + displayTerm: "Regulatory region variant", + impact: "MODIFIER", + rank: 38, + }, + "SO:0001628": { + color: "#636363", + displayTerm: "Intergenic variant", + impact: "MODIFIER", + rank: 39, + }, + "SO:0001060": { color: "#636363", displayTerm: "Sequence variant", impact: "MODIFIER", rank: 40 }, +}; + // Population Mapping export const populationMap: { [key: string]: string } = { fin: "Finnish", From 74841158693e73755eeeb3fe6f8abd7fbd2b9c11 Mon Sep 17 00:00:00 2001 From: Graham McNeill Date: Mon, 24 Aug 2026 12:32:43 +0100 Subject: [PATCH 06/11] comments --- .../src/pages/MetricsPage/MetricsPage.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx index d65e952d0..5e12dab77 100644 --- a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx +++ b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx @@ -22,24 +22,27 @@ function MetricsPage() { <> Data Metrics - Polish: do these look like buttons? + {/* Polish: do these look like buttons? Todo: finalise card order and icons - what for coloc? evidence and cred sets ok to be same? more info in tooltip where approp? - e.g. explain a prioritised gene - Alternative: more hierarchical, e.g. split into top-level entity counts then evidence linking targets and diseases, credible sets and colocs in variants section. + Alternative: more hierarchical, e.g. split into top-level entity counts then evidence linking targets and diseases, credible sets and colocs in variants section. */} - Coverage + Coverage - Alternative: Replace Other+tooltip with 'show more'? +
+ {/* Alternative: Replace Other+tooltip with 'show more'? */} - Alternative: +
+ {/* Alternative: */} - Alternative: + {/* Alternative: */} - Genetics + Genetics - Alternative: +
+ {/* Alternative: */} - Polish:Can we remove "variant" from every bar label? - Alternative:Replace Other+tooltip with 'show more'? + {/* Polish:Can we remove "variant" from every bar label? */} + {/* Alternative:Replace Other+tooltip with 'show more'? */} ); } From 21cc63c8fc2d93d2fa7ffd5869b5171d7fd4be29 Mon Sep 17 00:00:00 2001 From: Graham McNeill Date: Mon, 31 Aug 2026 11:02:13 +0100 Subject: [PATCH 07/11] add and remove cards and plots --- .../src/pages/MetricsPage/MetricsCards.tsx | 15 ++++++--------- .../src/pages/MetricsPage/MetricsPage.tsx | 3 +-- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/apps/platform/src/pages/MetricsPage/MetricsCards.tsx b/apps/platform/src/pages/MetricsPage/MetricsCards.tsx index 18b5c19ba..7b5d12ee6 100644 --- a/apps/platform/src/pages/MetricsPage/MetricsCards.tsx +++ b/apps/platform/src/pages/MetricsPage/MetricsCards.tsx @@ -20,10 +20,7 @@ const count = (rows: MetricRow[], dataset: string) => const formatRoundedCount = (value: number) => { if (value === 0) return "0"; - const unitExponent = Math.max(0, Math.floor(Math.log10(Math.abs(value)) / 3)); - const unit = 10 ** (unitExponent * 3); - const roundedValue = Math.round((value / unit) * 10) / 10; - return format("~s")(roundedValue * unit); + return format(".2~s")(value); }; function MetricsCards({ data }: { data: MetricRow[] }) { @@ -31,13 +28,13 @@ function MetricsCards({ data }: { data: MetricRow[] }) { ["Targets", faDna, count(data, "target")], ["Diseases", faStethoscope, count(data, "disease")], ["Drugs", faPrescriptionBottleMedical, count(data, "drug_molecule")], - ["Studies", faChartBar, count(data, "study")], + ["Clinical reports", faChartBar, count(data, "clinical_report")], + ["GWAS", faChartBar, count(data, "study")], ["Credible sets", faProjectDiagram, count(data, "credible_set")], - ["Evidence", faProjectDiagram, data.filter((row) => row.dataset.startsWith("evidence_") && row.metric === "count").reduce((sum, row) => sum + row.value, 0)], + ["Direct target-disease association", faProjectDiagram, count(data, "association_overall_direct")], + ["Indirect target-disease association", faProjectDiagram, count(data, "association_overall_indirect")], + ["Target-disease Evidence", faProjectDiagram, data.filter((row) => row.dataset.startsWith("evidence_") && row.metric === "count").reduce((sum, row) => sum + row.value, 0)], ["Variants", faMapPin, count(data, "variant")], - ["Prioritised genes", faDna, data.find((row) => row.dataset === "l2g_prediction" && row.kind === "filter" && row.metric === "prioritised_genes")?.value ?? 0], - ["GWAS/GWAS colocs", faCircleNodes, data.find((row) => row.dataset === "colocalisation" && row.group_value === "gwas-gwas")?.value ?? 0], - ["GWAS/QTL colocs", faCircleNodes, data.find((row) => row.dataset === "colocalisation" && row.group_value === "gwas-eqtl")?.value ?? 0], ] as const; return ( diff --git a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx index 5e12dab77..6eb8dc172 100644 --- a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx +++ b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx @@ -27,8 +27,7 @@ function MetricsPage() { Alternative: more hierarchical, e.g. split into top-level entity counts then evidence linking targets and diseases, credible sets and colocs in variants section. */} Coverage - -
+ {/* Alternative: Replace Other+tooltip with 'show more'? */}
From c577bdd083bd15333624f41ab0ffc720dee4c207 Mon Sep 17 00:00:00 2001 From: Graham McNeill Date: Mon, 31 Aug 2026 14:04:59 +0100 Subject: [PATCH 08/11] add clinical reports plot and order stages in plots correctly --- .../MetricsPage/ClinicalReportsByStage.tsx | 14 ++ .../pages/MetricsPage/ClinicalStageChart.tsx | 60 +++++++++ .../MetricsPage/DrugsByClinicalStage.tsx | 121 +----------------- .../src/pages/MetricsPage/MetricsPage.tsx | 3 + 4 files changed, 79 insertions(+), 119 deletions(-) create mode 100644 apps/platform/src/pages/MetricsPage/ClinicalReportsByStage.tsx create mode 100644 apps/platform/src/pages/MetricsPage/ClinicalStageChart.tsx diff --git a/apps/platform/src/pages/MetricsPage/ClinicalReportsByStage.tsx b/apps/platform/src/pages/MetricsPage/ClinicalReportsByStage.tsx new file mode 100644 index 000000000..c31e228fe --- /dev/null +++ b/apps/platform/src/pages/MetricsPage/ClinicalReportsByStage.tsx @@ -0,0 +1,14 @@ +import ClinicalStageChart from "./ClinicalStageChart"; +import type { MetricRow } from "./MetricsPage"; + +function ClinicalReportsByStage({ data }: { data: MetricRow[] }) { + return ( + + ); +} + +export default ClinicalReportsByStage; diff --git a/apps/platform/src/pages/MetricsPage/ClinicalStageChart.tsx b/apps/platform/src/pages/MetricsPage/ClinicalStageChart.tsx new file mode 100644 index 000000000..3910f1715 --- /dev/null +++ b/apps/platform/src/pages/MetricsPage/ClinicalStageChart.tsx @@ -0,0 +1,60 @@ +import { Box, Paper, Typography, useTheme } from "@mui/material"; +import * as Plot from "@observablehq/plot"; +import { clinicalStageCategories } from "@ot/constants"; +import { ObsPlot } from "ui"; +import type { MetricRow } from "./MetricsPage"; + +type ClinicalStageCount = { name: string; count: number; index: number }; + +function ClinicalStageChart({ data, dataset, title }: { data: MetricRow[]; dataset: string; title: string }) { + const theme = useTheme(); + const chartData: ClinicalStageCount[] = data + .filter((row) => row.dataset === dataset && row.kind === "grouping" && row.metric === "clinicalStage" && row.group_value) + .map((row) => { + const category = clinicalStageCategories[row.group_value as keyof typeof clinicalStageCategories]; + return { name: category?.label ?? row.group_value, count: row.value, index: category?.index ?? Number.MAX_SAFE_INTEGER }; + }) + .sort((a, b) => a.index - b.index); + + if (chartData.length === 0) return null; + + return ( + + + {title} + item.count} + yTooltip={(item) => item.name} + xAnchorTooltip="adapt" + yAnchorTooltip="adapt" + gapInfo={0} + renderInfo={() => null} + /> + + + ); + + function renderChart({ data, width, height }: { data: ClinicalStageCount[]; width?: number; height: number }) { + const plotWidth = Math.max((width ?? 0) - 210 - 24, 0); + const maxCount = Math.max(...data.map((item) => item.count)); + const insideData = data.filter((item) => (item.count / maxCount) * plotWidth >= `${item.count.toLocaleString()}`.length * 7 + 12); + const outsideData = data.filter((item) => !insideData.includes(item)); + + return Plot.plot({ + width: width ?? 0, height, style: { fontSize: "13.5px" }, marginTop: 4, marginBottom: 4, marginLeft: 240, marginRight: 0, + x: { axis: null }, y: { domain: data.map((item) => item.name), label: null, tickSize: 0, tickPadding: 8 }, + marks: [ + Plot.barX(data, { x: "count", y: "name", fill: theme.palette.primary.main, insetTop: 2, insetBottom: 2, className: "obs-tooltip" }), + Plot.text(insideData, { x: "count", y: "name", text: (item) => item.count.toLocaleString(), textAnchor: "end", dx: -6, fill: "white", lineAnchor: "middle", fontSize: 12.5, className: "obs-tooltip" }), + Plot.text(outsideData, { x: "count", y: "name", text: (item) => item.count.toLocaleString(), textAnchor: "start", dx: 6, fill: theme.palette.text.primary, lineAnchor: "middle", fontSize: 12.5, className: "obs-tooltip" }), + ], + }); + } +} + +export default ClinicalStageChart; diff --git a/apps/platform/src/pages/MetricsPage/DrugsByClinicalStage.tsx b/apps/platform/src/pages/MetricsPage/DrugsByClinicalStage.tsx index 77f12d3ee..672b40cb9 100644 --- a/apps/platform/src/pages/MetricsPage/DrugsByClinicalStage.tsx +++ b/apps/platform/src/pages/MetricsPage/DrugsByClinicalStage.tsx @@ -1,125 +1,8 @@ -import { Box, Paper, Typography, useTheme } from "@mui/material"; -import * as Plot from "@observablehq/plot"; -import { clinicalStageCategories } from "@ot/constants"; -import { ObsPlot } from "ui"; +import ClinicalStageChart from "./ClinicalStageChart"; import type { MetricRow } from "./MetricsPage"; -type ClinicalStageCount = { name: string; count: number }; - function DrugsByClinicalStage({ data }: { data: MetricRow[] }) { - const theme = useTheme(); - const chartData: ClinicalStageCount[] = data - .filter( - (row) => - row.dataset === "drug_molecule" && - row.kind === "grouping" && - row.metric === "clinicalStage" && - row.group_value, - ) - .map((row) => ({ - name: - clinicalStageCategories[row.group_value as keyof typeof clinicalStageCategories]?.label ?? - row.group_value, - count: row.value, - })) - .sort((a, b) => b.count - a.count); - - if (chartData.length === 0) return null; - - return ( - - - - Drugs by clinical stage - - item.count} - yTooltip={(item) => item.name} - xAnchorTooltip="adapt" - yAnchorTooltip="adapt" - gapInfo={0} - renderInfo={() => null} - /> - - - ); - - function renderChart({ - data, - width, - height, - }: { - data: ClinicalStageCount[]; - width?: number; - height: number; - }) { - const plotWidth = Math.max((width ?? 0) - 210 - 24, 0); - const maxCount = Math.max(...data.map((item) => item.count)); - const insideData = data.filter( - (item) => - (item.count / maxCount) * plotWidth >= - `${item.count.toLocaleString()}`.length * 7 + 12, - ); - const outsideData = data.filter((item) => !insideData.includes(item)); - - return Plot.plot({ - width: width ?? 0, - height, - style: { fontSize: "13.5px" }, - marginTop: 4, - marginBottom: 4, - marginLeft: 240, - marginRight: 0, - x: { axis: null }, - y: { - domain: data.map((item) => item.name), - label: null, - tickSize: 0, - tickPadding: 8, - }, - marks: [ - Plot.barX(data, { - x: "count", - y: "name", - fill: theme.palette.primary.main, - insetTop: 2, - insetBottom: 2, - className: "obs-tooltip", - }), - Plot.text(insideData, { - x: "count", - y: "name", - text: (item) => item.count.toLocaleString(), - textAnchor: "end", - dx: -6, - fill: "white", - lineAnchor: "middle", - fontSize: 12.5, - className: "obs-tooltip", - }), - Plot.text(outsideData, { - x: "count", - y: "name", - text: (item) => item.count.toLocaleString(), - textAnchor: "start", - dx: 6, - fill: theme.palette.text.primary, - lineAnchor: "middle", - fontSize: 12.5, - className: "obs-tooltip", - }), - ], - }); - } + return ; } export default DrugsByClinicalStage; diff --git a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx index 6eb8dc172..7c345b92a 100644 --- a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx +++ b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx @@ -6,6 +6,7 @@ import MetricsCards from "./MetricsCards"; import DiseasesByTherapeuticArea from "./DiseasesByTherapeuticArea"; import EvidenceByDataType from "./EvidenceByDataType"; import DrugsByClinicalStage from "./DrugsByClinicalStage"; +import ClinicalReportsByStage from "./ClinicalReportsByStage"; import CredibleSetsByStudyType from "./CredibleSetsByStudyType"; import VariantsByConsequence from "./VariantsByConsequence"; @@ -33,6 +34,8 @@ function MetricsPage() {
{/* Alternative: */} +
+ {/* Alternative: */} Genetics From 8f0ddb91a9962a6e4976fa9b265f0d95f5056d8f Mon Sep 17 00:00:00 2001 From: Graham McNeill Date: Tue, 1 Sep 2026 12:38:07 +0100 Subject: [PATCH 09/11] add associations plots --- .../src/pages/MetricsPage/AssociationPlot.tsx | 33 +++++ .../pages/MetricsPage/EvidenceByDataType.tsx | 126 ------------------ .../src/pages/MetricsPage/EvidenceChart.tsx | 8 ++ .../src/pages/MetricsPage/MetricsPage.tsx | 9 +- 4 files changed, 48 insertions(+), 128 deletions(-) create mode 100644 apps/platform/src/pages/MetricsPage/AssociationPlot.tsx delete mode 100644 apps/platform/src/pages/MetricsPage/EvidenceByDataType.tsx create mode 100644 apps/platform/src/pages/MetricsPage/EvidenceChart.tsx diff --git a/apps/platform/src/pages/MetricsPage/AssociationPlot.tsx b/apps/platform/src/pages/MetricsPage/AssociationPlot.tsx new file mode 100644 index 000000000..67bd9c013 --- /dev/null +++ b/apps/platform/src/pages/MetricsPage/AssociationPlot.tsx @@ -0,0 +1,33 @@ +import { Box, Paper, Typography, useTheme } from "@mui/material"; +import * as Plot from "@observablehq/plot"; +import { ObsPlot } from "ui"; +import dataSourcesAssoc from "../../components/AssociationsToolkit/static_datasets/dataSourcesAssoc"; +import { CATEGORICAL_COLORS } from "../../components/AssociationsToolkit/components/Table/NoveltyCharts"; +import type { MetricRow } from "./MetricsPage"; + +type AssociationCount = { name: string; group: string; count: number }; +const dataSourceTypes = new Map(dataSourcesAssoc.map((source) => [source.id, source.aggregation])); + +function AssociationPlot({ data, datasetPrefix, title, labelFromDataset = false }: { data: MetricRow[]; datasetPrefix: string; title: string; labelFromDataset?: boolean }) { + const theme = useTheme(); + const chartData: AssociationCount[] = data.filter((row) => row.dataset.startsWith(datasetPrefix) && row.kind === "grouping" && row.group_value) + .map((row) => { + const sourceId = labelFromDataset ? row.dataset.replace(datasetPrefix, "") : row.group_value; + return { name: labelFromDataset ? sourceId : row.group_value, group: dataSourceTypes.get(sourceId) ?? "Other", count: row.value }; + }); + const groupMaxCounts = new Map(); + chartData.forEach((item) => groupMaxCounts.set(item.group, Math.max(groupMaxCounts.get(item.group) ?? 0, item.count))); + chartData.sort((a, b) => (groupMaxCounts.get(b.group) ?? 0) - (groupMaxCounts.get(a.group) ?? 0) || a.group.localeCompare(b.group) || b.count - a.count); + if (chartData.length === 0) return null; + return + {title} + item.count} yTooltip={(item) => item.name} xAnchorTooltip="adapt" yAnchorTooltip="adapt" gapInfo={0} renderInfo={() => null} /> + ; + function renderChart({ data, width, height }: { data: AssociationCount[]; width?: number; height: number }) { + const plotWidth = Math.max((width ?? 0) - 210 - 24, 0), maxCount = Math.max(...data.map((item) => item.count)); + const insideData = data.filter((item) => (item.count / maxCount) * plotWidth >= `${item.count.toLocaleString()}`.length * 7 + 12); + const outsideData = data.filter((item) => !insideData.includes(item)); + return Plot.plot({ width: width ?? 0, height, style: { fontSize: "13.5px" }, marginTop: 4, marginBottom: 4, marginLeft: 240, marginRight: 0, x: { axis: null }, color: { domain: [...new Set(data.map((item) => item.group))], range: CATEGORICAL_COLORS, legend: true }, y: { domain: data.map((item) => item.name), label: null, tickSize: 0, tickPadding: 8, tickFormat: (name) => name.replaceAll("_", " ") }, marks: [Plot.barX(data, { x: "count", y: "name", fill: "group", insetTop: 2, insetBottom: 2, className: "obs-tooltip" }), Plot.text(insideData, { x: "count", y: "name", text: (item) => item.count.toLocaleString(), textAnchor: "end", dx: -6, fill: "white", lineAnchor: "middle", fontSize: 12.5, className: "obs-tooltip" }), Plot.text(outsideData, { x: "count", y: "name", text: (item) => item.count.toLocaleString(), textAnchor: "start", dx: 6, fill: theme.palette.text.primary, lineAnchor: "middle", fontSize: 12.5, className: "obs-tooltip" })] }); + } +} +export default AssociationPlot; diff --git a/apps/platform/src/pages/MetricsPage/EvidenceByDataType.tsx b/apps/platform/src/pages/MetricsPage/EvidenceByDataType.tsx deleted file mode 100644 index 719514804..000000000 --- a/apps/platform/src/pages/MetricsPage/EvidenceByDataType.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { Box, Paper, Typography, useTheme } from "@mui/material"; -import * as Plot from "@observablehq/plot"; -import { ObsPlot } from "ui"; -import type { MetricRow } from "./MetricsPage"; - -type DataTypeCount = { name: string; count: number }; - -function EvidenceByDataType({ data }: { data: MetricRow[] }) { - const theme = useTheme(); - const counts = new Map(); - - data - .filter( - (row) => - row.dataset.startsWith("evidence_") && - row.kind === "grouping" && - row.group_value, - ) - .forEach((row) => - counts.set(row.group_value, (counts.get(row.group_value) ?? 0) + row.value), - ); - - const chartData: DataTypeCount[] = [...counts] - .map(([name, count]) => ({ name, count })) - .sort((a, b) => b.count - a.count); - - if (chartData.length === 0) return null; - - return ( - - - - Evidence by data type - - item.count} - yTooltip={(item) => item.name} - xAnchorTooltip="adapt" - yAnchorTooltip="adapt" - gapInfo={0} - renderInfo={() => null} - /> - - - ); - - function renderChart({ - data, - width, - height, - }: { - data: DataTypeCount[]; - width?: number; - height: number; - }) { - const plotWidth = Math.max((width ?? 0) - 210 - 24, 0); - const maxCount = Math.max(...data.map((item) => item.count)); - const insideData = data.filter( - (item) => - (item.count / maxCount) * plotWidth >= - `${item.count.toLocaleString()}`.length * 7 + 12, - ); - const outsideData = data.filter((item) => !insideData.includes(item)); - - return Plot.plot({ - width: width ?? 0, - height, - style: { fontSize: "13.5px" }, - marginTop: 4, - marginBottom: 4, - marginLeft: 240, - marginRight: 0, - x: { axis: null }, - y: { - domain: data.map((item) => item.name), - label: null, - tickSize: 0, - tickPadding: 8, - tickFormat: (name) => name.replaceAll("_", " "), - }, - marks: [ - Plot.barX(data, { - x: "count", - y: "name", - fill: theme.palette.primary.main, - insetTop: 2, - insetBottom: 2, - className: "obs-tooltip", - }), - Plot.text(insideData, { - x: "count", - y: "name", - text: (item) => item.count.toLocaleString(), - textAnchor: "end", - dx: -6, - fill: "white", - lineAnchor: "middle", - fontSize: 12.5, - className: "obs-tooltip", - }), - Plot.text(outsideData, { - x: "count", - y: "name", - text: (item) => item.count.toLocaleString(), - textAnchor: "start", - dx: 6, - fill: theme.palette.text.primary, - lineAnchor: "middle", - fontSize: 12.5, - className: "obs-tooltip", - }), - ], - }); - } -} - -export default EvidenceByDataType; diff --git a/apps/platform/src/pages/MetricsPage/EvidenceChart.tsx b/apps/platform/src/pages/MetricsPage/EvidenceChart.tsx new file mode 100644 index 000000000..0ff2c8917 --- /dev/null +++ b/apps/platform/src/pages/MetricsPage/EvidenceChart.tsx @@ -0,0 +1,8 @@ +import AssociationPlot from "./AssociationPlot"; +import type { MetricRow } from "./MetricsPage"; + +function EvidenceChart({ data }: { data: MetricRow[] }) { + return ; +} + +export default EvidenceChart; diff --git a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx index 7c345b92a..19e5be3a5 100644 --- a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx +++ b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx @@ -4,7 +4,8 @@ import { Typography } from "@mui/material"; import metricsCsv from "./metrics.csv?url"; import MetricsCards from "./MetricsCards"; import DiseasesByTherapeuticArea from "./DiseasesByTherapeuticArea"; -import EvidenceByDataType from "./EvidenceByDataType"; +import EvidenceChart from "./EvidenceChart"; +import AssociationPlot from "./AssociationPlot"; import DrugsByClinicalStage from "./DrugsByClinicalStage"; import ClinicalReportsByStage from "./ClinicalReportsByStage"; import CredibleSetsByStudyType from "./CredibleSetsByStudyType"; @@ -30,7 +31,11 @@ function MetricsPage() { Coverage {/* Alternative: Replace Other+tooltip with 'show more'? */} - + +
+ +
+
{/* Alternative: */} From 0852a5375fcdc5f0c36e57d15af11a8ee6da7398 Mon Sep 17 00:00:00 2001 From: Graham McNeill Date: Tue, 1 Sep 2026 15:38:13 +0100 Subject: [PATCH 10/11] add study by type plot --- .../src/pages/MetricsPage/ByStudyType.tsx | 34 +++++ .../MetricsPage/CredibleSetsByStudyType.tsx | 120 +----------------- .../src/pages/MetricsPage/MetricsPage.tsx | 3 + .../pages/MetricsPage/StudiesByStudyType.tsx | 8 ++ 4 files changed, 47 insertions(+), 118 deletions(-) create mode 100644 apps/platform/src/pages/MetricsPage/ByStudyType.tsx create mode 100644 apps/platform/src/pages/MetricsPage/StudiesByStudyType.tsx diff --git a/apps/platform/src/pages/MetricsPage/ByStudyType.tsx b/apps/platform/src/pages/MetricsPage/ByStudyType.tsx new file mode 100644 index 000000000..4e996dd72 --- /dev/null +++ b/apps/platform/src/pages/MetricsPage/ByStudyType.tsx @@ -0,0 +1,34 @@ +import { Box, Paper, Typography, useTheme } from "@mui/material"; +import * as Plot from "@observablehq/plot"; +import { ObsPlot } from "ui"; +import type { MetricRow } from "./MetricsPage"; + +type StudyTypeCount = { name: string; count: number }; + +function ByStudyType({ data, dataset, title }: { data: MetricRow[]; dataset: string; title: string }) { + const theme = useTheme(); + const chartData: StudyTypeCount[] = data + .filter((row) => row.dataset === dataset && row.kind === "grouping" && row.expression === "studyType" && row.group_value) + .map((row) => ({ name: row.group_value, count: row.value })) + .sort((a, b) => b.count - a.count); + if (chartData.length === 0) return null; + + return + {title} + item.count} yTooltip={(item) => item.name} xAnchorTooltip="adapt" yAnchorTooltip="adapt" gapInfo={0} renderInfo={() => null} /> + ; + + function renderChart({ data, width, height }: { data: StudyTypeCount[]; width?: number; height: number }) { + const plotWidth = Math.max((width ?? 0) - 210 - 24, 0); + const maxCount = Math.max(...data.map((item) => item.count)); + const insideData = data.filter((item) => (item.count / maxCount) * plotWidth >= `${item.count.toLocaleString()}`.length * 7 + 12); + const outsideData = data.filter((item) => !insideData.includes(item)); + return Plot.plot({ width: width ?? 0, height, style: { fontSize: "13.5px" }, marginTop: 4, marginBottom: 4, marginLeft: 240, marginRight: 0, x: { axis: null }, y: { domain: data.map((item) => item.name), label: null, tickSize: 0, tickPadding: 8, tickFormat: (label) => label.endsWith("qtl") ? `${label.slice(0, -3)}QTL` : label.toUpperCase() }, marks: [ + Plot.barX(data, { x: "count", y: "name", fill: theme.palette.primary.main, insetTop: 2, insetBottom: 2, className: "obs-tooltip" }), + Plot.text(insideData, { x: "count", y: "name", text: (item) => item.count.toLocaleString(), textAnchor: "end", dx: -6, fill: "white", lineAnchor: "middle", fontSize: 12.5, className: "obs-tooltip" }), + Plot.text(outsideData, { x: "count", y: "name", text: (item) => item.count.toLocaleString(), textAnchor: "start", dx: 6, fill: theme.palette.text.primary, lineAnchor: "middle", fontSize: 12.5, className: "obs-tooltip" }), + ] }); + } +} + +export default ByStudyType; diff --git a/apps/platform/src/pages/MetricsPage/CredibleSetsByStudyType.tsx b/apps/platform/src/pages/MetricsPage/CredibleSetsByStudyType.tsx index 2cf95a6a4..15b306c39 100644 --- a/apps/platform/src/pages/MetricsPage/CredibleSetsByStudyType.tsx +++ b/apps/platform/src/pages/MetricsPage/CredibleSetsByStudyType.tsx @@ -1,124 +1,8 @@ -import { Box, Paper, Typography, useTheme } from "@mui/material"; -import * as Plot from "@observablehq/plot"; -import { ObsPlot } from "ui"; +import ByStudyType from "./ByStudyType"; import type { MetricRow } from "./MetricsPage"; -type StudyTypeCount = { name: string; count: number }; - function CredibleSetsByStudyType({ data }: { data: MetricRow[] }) { - const theme = useTheme(); - const chartData: StudyTypeCount[] = data - .filter( - (row) => - row.dataset === "credible_set" && - row.kind === "grouping" && - row.expression === "studyType" && - row.group_value, - ) - .map((row) => ({ name: row.group_value, count: row.value })) - .sort((a, b) => b.count - a.count); - - if (chartData.length === 0) return null; - - return ( - - - - Credible sets by study type - - item.count} - yTooltip={(item) => item.name} - xAnchorTooltip="adapt" - yAnchorTooltip="adapt" - gapInfo={0} - renderInfo={() => null} - /> - - - ); - - function renderChart({ - data, - width, - height, - }: { - data: StudyTypeCount[]; - width?: number; - height: number; - }) { - const plotWidth = Math.max((width ?? 0) - 210 - 24, 0); - const maxCount = Math.max(...data.map((item) => item.count)); - const insideData = data.filter( - (item) => - (item.count / maxCount) * plotWidth >= - `${item.count.toLocaleString()}`.length * 7 + 12, - ); - const outsideData = data.filter((item) => !insideData.includes(item)); - - return Plot.plot({ - width: width ?? 0, - height, - style: { fontSize: "13.5px" }, - marginTop: 4, - marginBottom: 4, - marginLeft: 240, - marginRight: 0, - x: { axis: null }, - y: { - domain: data.map((item) => item.name), - label: null, - tickSize: 0, - tickPadding: 8, - tickFormat: label => { - return label.endsWith('qtl') - ? `${label.slice(0, -3)}QTL` - : label.toUpperCase() - } - }, - marks: [ - Plot.barX(data, { - x: "count", - y: "name", - fill: theme.palette.primary.main, - insetTop: 2, - insetBottom: 2, - className: "obs-tooltip", - }), - Plot.text(insideData, { - x: "count", - y: "name", - text: (item) => item.count.toLocaleString(), - textAnchor: "end", - dx: -6, - fill: "white", - lineAnchor: "middle", - fontSize: 12.5, - className: "obs-tooltip", - }), - Plot.text(outsideData, { - x: "count", - y: "name", - text: (item) => item.count.toLocaleString(), - textAnchor: "start", - dx: 6, - fill: theme.palette.text.primary, - lineAnchor: "middle", - fontSize: 12.5, - className: "obs-tooltip", - }), - ], - }); - } + return ; } export default CredibleSetsByStudyType; diff --git a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx index 19e5be3a5..b57affcca 100644 --- a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx +++ b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx @@ -9,6 +9,7 @@ import AssociationPlot from "./AssociationPlot"; import DrugsByClinicalStage from "./DrugsByClinicalStage"; import ClinicalReportsByStage from "./ClinicalReportsByStage"; import CredibleSetsByStudyType from "./CredibleSetsByStudyType"; +import StudiesByStudyType from "./StudiesByStudyType"; import VariantsByConsequence from "./VariantsByConsequence"; export type MetricRow = { dataset: string; kind: string; metric: string; group_value: string; value: number }; @@ -44,6 +45,8 @@ function MetricsPage() { {/* Alternative: */} Genetics + +

{/* Alternative: */} diff --git a/apps/platform/src/pages/MetricsPage/StudiesByStudyType.tsx b/apps/platform/src/pages/MetricsPage/StudiesByStudyType.tsx new file mode 100644 index 000000000..e159761ea --- /dev/null +++ b/apps/platform/src/pages/MetricsPage/StudiesByStudyType.tsx @@ -0,0 +1,8 @@ +import ByStudyType from "./ByStudyType"; +import type { MetricRow } from "./MetricsPage"; + +function StudiesByStudyType({ data }: { data: MetricRow[] }) { + return ; +} + +export default StudiesByStudyType; From 6307c76a313ce280b12d269c55d76ce4c4bd799b Mon Sep 17 00:00:00 2001 From: Graham McNeill Date: Tue, 1 Sep 2026 16:18:17 +0100 Subject: [PATCH 11/11] add coloc by type plot --- .../src/pages/MetricsPage/ByStudyType.tsx | 2 +- .../MetricsPage/ColocalisationByType.tsx | 69 +++++++++++++++++++ .../src/pages/MetricsPage/MetricsPage.tsx | 3 + 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 apps/platform/src/pages/MetricsPage/ColocalisationByType.tsx diff --git a/apps/platform/src/pages/MetricsPage/ByStudyType.tsx b/apps/platform/src/pages/MetricsPage/ByStudyType.tsx index 4e996dd72..d8d261e2f 100644 --- a/apps/platform/src/pages/MetricsPage/ByStudyType.tsx +++ b/apps/platform/src/pages/MetricsPage/ByStudyType.tsx @@ -23,7 +23,7 @@ function ByStudyType({ data, dataset, title }: { data: MetricRow[]; dataset: str const maxCount = Math.max(...data.map((item) => item.count)); const insideData = data.filter((item) => (item.count / maxCount) * plotWidth >= `${item.count.toLocaleString()}`.length * 7 + 12); const outsideData = data.filter((item) => !insideData.includes(item)); - return Plot.plot({ width: width ?? 0, height, style: { fontSize: "13.5px" }, marginTop: 4, marginBottom: 4, marginLeft: 240, marginRight: 0, x: { axis: null }, y: { domain: data.map((item) => item.name), label: null, tickSize: 0, tickPadding: 8, tickFormat: (label) => label.endsWith("qtl") ? `${label.slice(0, -3)}QTL` : label.toUpperCase() }, marks: [ + return Plot.plot({ width: width ?? 0, height, style: { fontSize: "13.5px" }, marginTop: 4, marginBottom: 4, marginLeft: 240, marginRight: 0, x: { axis: null }, y: { domain: data.map((item) => item.name), label: null, tickSize: 0, tickPadding: 8, tickFormat: (label) => label.replaceAll(/(gwas|qtl)/ig, m => m.toUpperCase()) }, marks: [ Plot.barX(data, { x: "count", y: "name", fill: theme.palette.primary.main, insetTop: 2, insetBottom: 2, className: "obs-tooltip" }), Plot.text(insideData, { x: "count", y: "name", text: (item) => item.count.toLocaleString(), textAnchor: "end", dx: -6, fill: "white", lineAnchor: "middle", fontSize: 12.5, className: "obs-tooltip" }), Plot.text(outsideData, { x: "count", y: "name", text: (item) => item.count.toLocaleString(), textAnchor: "start", dx: 6, fill: theme.palette.text.primary, lineAnchor: "middle", fontSize: 12.5, className: "obs-tooltip" }), diff --git a/apps/platform/src/pages/MetricsPage/ColocalisationByType.tsx b/apps/platform/src/pages/MetricsPage/ColocalisationByType.tsx new file mode 100644 index 000000000..82a8a337e --- /dev/null +++ b/apps/platform/src/pages/MetricsPage/ColocalisationByType.tsx @@ -0,0 +1,69 @@ +import { Box, Paper, Typography, useTheme } from "@mui/material"; +import * as Plot from "@observablehq/plot"; +import { ObsPlot } from "ui"; +import type { MetricRow } from "./MetricsPage"; + +type ColocalisationCount = { name: string; count: number }; + +function ColocalisationByType({ data }: { data: MetricRow[] }) { + const theme = useTheme(); + const chartData: ColocalisationCount[] = data + .filter((row) => row.dataset === "colocalisation" && row.kind === "grouping" && row.metric === "studyTypePair" && row.group_value) + .map((row) => ({ name: row.group_value, count: row.value })) + .sort((a, b) => b.count - a.count); + + if (chartData.length === 0) return null; + + return ( + + + Colocalisation by type + item.count} + yTooltip={(item) => item.name} + xAnchorTooltip="adapt" + yAnchorTooltip="adapt" + gapInfo={0} + renderInfo={() => null} + /> + + + ); + + function renderChart({ data, width, height }: { data: ColocalisationCount[]; width?: number; height: number }) { + const plotWidth = Math.max((width ?? 0) - 210 - 24, 0); + const maxCount = Math.max(...data.map((item) => item.count)); + const insideData = data.filter((item) => (item.count / maxCount) * plotWidth >= `${item.count.toLocaleString()}`.length * 7 + 12); + const outsideData = data.filter((item) => !insideData.includes(item)); + + return Plot.plot({ + width: width ?? 0, + height, + style: { fontSize: "13.5px" }, + marginTop: 4, + marginBottom: 4, + marginLeft: 240, + marginRight: 0, + x: { axis: null }, + y: { + domain: data.map((item) => item.name), + label: null, + tickSize: 0, + tickPadding: 8, + tickFormat: (label) => label.replaceAll(/(gwas|qtl)/ig, m => m.toUpperCase()), + }, + marks: [ + Plot.barX(data, { x: "count", y: "name", fill: theme.palette.primary.main, insetTop: 2, insetBottom: 2, className: "obs-tooltip" }), + Plot.text(insideData, { x: "count", y: "name", text: (item) => item.count.toLocaleString(), textAnchor: "end", dx: -6, fill: "white", lineAnchor: "middle", fontSize: 12.5, className: "obs-tooltip" }), + Plot.text(outsideData, { x: "count", y: "name", text: (item) => item.count.toLocaleString(), textAnchor: "start", dx: 6, fill: theme.palette.text.primary, lineAnchor: "middle", fontSize: 12.5, className: "obs-tooltip" }), + ], + }); + } +} + +export default ColocalisationByType; diff --git a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx index b57affcca..759dca4e6 100644 --- a/apps/platform/src/pages/MetricsPage/MetricsPage.tsx +++ b/apps/platform/src/pages/MetricsPage/MetricsPage.tsx @@ -9,6 +9,7 @@ import AssociationPlot from "./AssociationPlot"; import DrugsByClinicalStage from "./DrugsByClinicalStage"; import ClinicalReportsByStage from "./ClinicalReportsByStage"; import CredibleSetsByStudyType from "./CredibleSetsByStudyType"; +import ColocalisationByType from "./ColocalisationByType"; import StudiesByStudyType from "./StudiesByStudyType"; import VariantsByConsequence from "./VariantsByConsequence"; @@ -50,6 +51,8 @@ function MetricsPage() {
{/* Alternative: */} + +
{/* Polish:Can we remove "variant" from every bar label? */} {/* Alternative:Replace Other+tooltip with 'show more'? */}