Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/platform/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ const router = createBrowserRouter([
path: "/api",
lazy: () => import("./pages/APIPage/APIPage").then(m => ({ Component: m.default })),
},
{
path: "/metrics",
lazy: () =>
import("./pages/MetricsPage/MetricsPageWrapper").then(m => ({
Component: m.default,
})),
},
{
path: "/search",
lazy: () =>
Expand Down
33 changes: 33 additions & 0 deletions apps/platform/src/pages/MetricsPage/AssociationPlot.tsx
Original file line number Diff line number Diff line change
@@ -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<string, number>();
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 <Paper sx={{ py: 2, px: 3, maxWidth: "100%" }} elevation={0} variant="outlined"><Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ m: 0 }}>{title}</Typography>
<ObsPlot data={chartData} otherData={{ textColor: theme.palette.text.primary }} minWidth={320} height={chartData.length * 23 + 8} renderChart={renderChart} xTooltip={(item) => item.count} yTooltip={(item) => item.name} xAnchorTooltip="adapt" yAnchorTooltip="adapt" gapInfo={0} renderInfo={() => null} />
</Box></Paper>;
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;
34 changes: 34 additions & 0 deletions apps/platform/src/pages/MetricsPage/ByStudyType.tsx
Original file line number Diff line number Diff line change
@@ -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 <Paper sx={{ py: 2, px: 3, maxWidth: "100%" }} elevation={0} variant="outlined"><Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ m: 0 }}>{title}</Typography>
<ObsPlot data={chartData} otherData={{ textColor: theme.palette.text.primary }} minWidth={320} height={chartData.length * 23 + 8} renderChart={renderChart} xTooltip={(item) => item.count} yTooltip={(item) => item.name} xAnchorTooltip="adapt" yAnchorTooltip="adapt" gapInfo={0} renderInfo={() => null} />
</Box></Paper>;

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.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 ByStudyType;
14 changes: 14 additions & 0 deletions apps/platform/src/pages/MetricsPage/ClinicalReportsByStage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import ClinicalStageChart from "./ClinicalStageChart";
import type { MetricRow } from "./MetricsPage";

function ClinicalReportsByStage({ data }: { data: MetricRow[] }) {
return (
<ClinicalStageChart
data={data}
dataset="clinical_report"
title="Clinical reports by stage"
/>
);
}

export default ClinicalReportsByStage;
60 changes: 60 additions & 0 deletions apps/platform/src/pages/MetricsPage/ClinicalStageChart.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Paper sx={{ py: 2, px: 3, maxWidth: "100%" }} elevation={0} variant="outlined">
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ m: 0 }}>{title}</Typography>
<ObsPlot
data={chartData}
otherData={{ textColor: theme.palette.text.primary }}
minWidth={320}
height={chartData.length * 23 + 8}
renderChart={renderChart}
xTooltip={(item) => item.count}
yTooltip={(item) => item.name}
xAnchorTooltip="adapt"
yAnchorTooltip="adapt"
gapInfo={0}
renderInfo={() => null}
/>
</Box>
</Paper>
);

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;
69 changes: 69 additions & 0 deletions apps/platform/src/pages/MetricsPage/ColocalisationByType.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Paper sx={{ py: 2, px: 3, maxWidth: "100%" }} elevation={0} variant="outlined">
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ m: 0 }}>Colocalisation by type</Typography>
<ObsPlot
data={chartData}
otherData={{ textColor: theme.palette.text.primary }}
minWidth={320}
height={chartData.length * 23 + 8}
renderChart={renderChart}
xTooltip={(item) => item.count}
yTooltip={(item) => item.name}
xAnchorTooltip="adapt"
yAnchorTooltip="adapt"
gapInfo={0}
renderInfo={() => null}
/>
</Box>
</Paper>
);

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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import ByStudyType from "./ByStudyType";
import type { MetricRow } from "./MetricsPage";

function CredibleSetsByStudyType({ data }: { data: MetricRow[] }) {
return <ByStudyType data={data} dataset="credible_set" title="Credible sets by study type" />;
}

export default CredibleSetsByStudyType;
Loading
Loading