diff --git a/app/components/Haddock3/BoxPlots.tsx b/app/components/Haddock3/BoxPlots.tsx index 86ac718b..64f478af 100644 --- a/app/components/Haddock3/BoxPlots.tsx +++ b/app/components/Haddock3/BoxPlots.tsx @@ -1,7 +1,5 @@ import type { Scores } from "./CaprievalReport.client"; export function BoxPlots({ scores }: { scores: Scores }) { - return ( -
Place holder for boxplots
- ) -} \ No newline at end of file + return
Place holder for boxplots
; +} diff --git a/app/components/Haddock3/ScatterPlots.tsx b/app/components/Haddock3/ScatterPlots.tsx index d1d6f2f1..1c5183e5 100644 --- a/app/components/Haddock3/ScatterPlots.tsx +++ b/app/components/Haddock3/ScatterPlots.tsx @@ -1,87 +1,271 @@ -import type { Scores } from "./CaprievalReport.client"; -import type { Layout, Data } from "plotly.js"; +import type { Layout, Data, LayoutAxis, AxisName } from "plotly.js"; import { useMemo } from "react"; import Plot from "react-plotly.js"; +import type { Scores } from "./CaprievalReport.client"; + +const SUBPLOTS = { + columns: ["score", "desolv", "vdw", "elec", "air"], + rows: ["irmsd", "dockq", "lrmsd", "ilrmsd"].reverse(), +}; -const SCATTER_AXES = { - x: ["score", "desolv", "vdw", "elec", "air"], - y: ["irmsd", "dockq", "lrmsd", "ilrmsd"], +const DOMAINS = { + columns: [ + [0.0, 0.152], + [0.212, 0.364], + [0.424, 0.576], + [0.636, 0.788], + [0.848, 1.0], + ], + rows: [ + [0, 0.175], + [0.275, 0.45], + [0.55, 0.725], + [0.825, 1.0], + ], }; -export function generateSubPlots(scores: Scores, axes = SCATTER_AXES): Data[] { +const TITLE_NAMES = { + "score": "HADDOCK score", + "irmsd": "i-RMSD", + "lrmsd": "l-RMSD", + "ilrmsd": "il-RMSD", + "dockq": "DOCKQ", + "desolv": "Edesolv", + "vdw": "Evdw", + "elec": "Eelec", + "air": "Eair", + "fnat": "FCC", +}; + +// Dark24 from venv/lib/python3.10/site-packages/_plotly_utils/colors/qualitative.py +const CLUSTER_COLORS = [ + "#2E91E5", + "#E15F99", + "#1CA71C", + "#FB0D0D", + "#DA16FF", + "#222A2A", + "#B68100", + "#750D86", + "#EB663B", + "#511CFB", + "#00A08B", + "#FB00D1", + "#FC0080", + "#B2828D", + "#6C7C32", + "#778AAE", + "#862A16", + "#A777F1", + "#620042", + "#1616A7", + "#DA60CA", + "#6C4516", + "#0D2A63", + "#AF0038", +]; + +const MAX_CLUSTER_TO_PLOT = 10; + +function generateSubPlots(scores: Scores, axes = SUBPLOTS): Data[] { const data: Data[] = []; - for (const x of axes.x) { - const xaxis = axes.x.indexOf(x) == 0 ? "x" : "x" + (axes.x.indexOf(x) + 1); - for (const y of axes.y) { - const yaxis = - axes.y.indexOf(y) == 0 ? "y" : "y" + (axes.y.indexOf(y) + 1); - scores.clusters.forEach((cluster) => { - const structures4cluster = scores.structures.filter( - (s) => s["cluster-id"] === cluster.cluster_id - ); - const name = cluster.cluster_id === "-" ? "Unclustered" : cluster.cluster_id + "" - data.push({ - x: structures4cluster.map((s) => s[x]), - y: structures4cluster.map((s) => s[y]), - name, - legendgroup: name, - mode: "markers", - type: "scatter", - // "Model: mdscoring_17.pdb
Score: -13.82
Caprieval rank: 16", - text: structures4cluster.map( - (s) => - `Model: ${s.model}
Score: ${s.score}
Caprieval rank: ${s.caprieval_rank}` - ), - xaxis, - yaxis, - }); - data.push({ - error_x: { - array: [cluster[x + "_std"]], - type: "data", - visible: true, - }, - error_y: { - array: [cluster[y + "_std"]], - type: "data", - visible: true, - }, - x: [cluster[x]], - y: [cluster[y]], - marker: { color: "#2E91E5", size: 10, symbol: "square-dot" }, - legendgroup: name, - mode: "markers", - showlegend: false, - type: "scatter", - xaxis, - yaxis, - }) - }); + let aIndex = 1; + for (const row of axes.rows) { + for (const column of axes.columns) { + generateSubPlot(aIndex, scores, row, column, data); + aIndex++; } } - + // console.log(data); return data; } +function generateSubPlot( + aIndex: number, + scores: Scores, + row: string, + column: string, + data: Data[] +) { + const xaxis = "x" + (aIndex === 1 ? "" : aIndex); + const yaxis = "y" + (aIndex === 1 ? "" : aIndex); + const other: Data = { + xaxis, + yaxis, + type: "scatter", + mode: "markers", + marker: { color: "white", line: { color: "DarkSlateGrey", width: 2 } }, + name: "Other", + legendgroup: "Other", + showlegend: aIndex === 1, + hoverlabel: { + bgcolor: "white", + font: { family: "Helvetica", size: 16 }, + }, + text: [], + x: [], + y: [], + }; + for (const cluster of scores.clusters) { + const structures4cluster = scores.structures.filter( + (s) => s["cluster-id"] === cluster.cluster_id + ); + if ( + cluster.cluster_rank === "-" || + Number(cluster.cluster_rank) <= MAX_CLUSTER_TO_PLOT + ) { + const color = + CLUSTER_COLORS[ + cluster.cluster_rank === "-" ? 0 : Number(cluster.cluster_rank) - 1 + ]; + const name = + "Cluster " + + (cluster.cluster_rank === "-" + ? "Unclustered" + : cluster.cluster_rank + ""); + data.push({ + x: structures4cluster.map((s) => s[row]), + y: structures4cluster.map((s) => s[column]), + name, + legendgroup: name, + hoverlabel: { + bgcolor: color, + font: { family: "Helvetica", size: 16 }, + }, + showlegend: aIndex === 1, + marker: { color }, + mode: "markers", + type: "scatter", + // "Model: mdscoring_17.pdb
Score: -13.82
Caprieval rank: 16", + text: structures4cluster.map( + (s) => + `Model: ${s.model}
Score: ${s.score}
Caprieval rank: ${s.caprieval_rank}` + ), + xaxis, + yaxis, + }); + // Error bars + data.push({ + error_x: { + array: [cluster[row + "_std"]], + type: "data", + visible: true, + }, + error_y: { + array: [cluster[column + "_std"]], + type: "data", + visible: true, + }, + x: [cluster[row]], + y: [cluster[column]], + marker: { color, size: 10, symbol: "square-dot" }, + text: [ + `Cluster ${cluster.cluster_id}
${column}: ${cluster[column]}
${row}: ${cluster[row]}`, + ], + hoverlabel: { + bgcolor: color, + font: { family: "Helvetica", size: 16 }, + }, + hovertemplate: `Cluster ${cluster.cluster_id}
${column}: ${cluster[column]}
${row}: ${cluster[row]}
`, + legendgroup: name, + mode: "markers", + showlegend: false, + type: "scatter", + xaxis, + yaxis, + }); + } else { + // Fill other cluster + other.x = (other.x as Array).concat( + structures4cluster.map((s) => s[row]) + ); + other.y = (other.y as Array).concat( + structures4cluster.map((s) => s[column]) + ); + other.text = (other.text as Array).concat( + structures4cluster.map( + (s) => + `Model: ${s.model}
Score: ${s.score}
Caprieval rank: ${s.caprieval_rank}` + ) + ); + } + } + if (other.x!.length > 0) { + data.push(other); + } +} + +function generateAxes() { + const axes: Record> = {}; + let aIndex = 1; + for (const row of SUBPLOTS.rows) { + for (const column of SUBPLOTS.columns) { + generatePlotAxes(aIndex, column, row, axes); + aIndex++; + } + } + // console.log(JSON.stringify(axes,null,2)); + return axes; +} + +function generatePlotAxes( + aIndex: number, + column: string, + row: string, + axes: Record> +) { + const xaxisKey = "xaxis" + (aIndex === 1 ? "" : aIndex); + const yaxisKey = "yaxis" + (aIndex === 1 ? "" : aIndex); + const ax = ("x" + (aIndex === 1 ? "" : aIndex)) as AxisName; + const ay = ("y" + (aIndex === 1 ? "" : aIndex)) as AxisName; + const colindex = SUBPLOTS.columns.indexOf(column); + const rowindex = SUBPLOTS.rows.indexOf(row); + const mx = + rowindex === 0 ? "x" : "x" + (rowindex + 1) * SUBPLOTS.columns.length; + const my = colindex === 0 ? "y" : "y" + (colindex + 1); + axes[xaxisKey] = { + anchor: ay, + domain: DOMAINS.columns[colindex], + title: { + text: TITLE_NAMES[row as keyof typeof TITLE_NAMES], + standoff: 5, + }, + automargin: true, + }; + axes[yaxisKey] = { + anchor: ax, + domain: DOMAINS.rows[rowindex], + title: { + text: TITLE_NAMES[column as keyof typeof TITLE_NAMES], + standoff: 5, + }, + automargin: true, + }; + if (mx !== ax) { + axes[xaxisKey].matches = mx; + } + if (my !== ay) { + axes[yaxisKey].matches = my; + } +} + export function ScatterPlots({ scores }: { scores: Scores }) { const data = useMemo(() => generateSubPlots(scores), [scores]); - const subplots = [ - ["xy", "x2y", "x3y", "x4y", "x5y"], - ["xy2", "x2y2", "x3y2", "x4y2", "x5y2"], - ["xy3", "x2y3", "x3y3", "x4y3", "x5y3"], - ["xy4", "x2y4", "x3y4", "x4y4", "x5y4"], - ] as any; + const axes = useMemo(() => generateAxes(), []); const layout: Partial = { - width: 1400, - height: 1750, - grid: { - rows: 4, - columns: 5, - pattern: "coupled", - subplots, - roworder: "bottom to top", - }, + height: 800, + width: 1300, + legend: { title: { text: "Cluster Rank" } }, + ...axes, }; - return ; + return ( + + ); } diff --git a/app/components/OutputReport.tsx b/app/components/OutputReport.tsx index 19a3b809..2e445712 100644 --- a/app/components/OutputReport.tsx +++ b/app/components/OutputReport.tsx @@ -11,7 +11,7 @@ export function files2modules(files: DirectoryItem) { const nonmodules = new Set(["analysis", "data", "log"]); return files.children .filter((i) => !nonmodules.has(i.name)) - .filter((i) => !i.name.endsWith('_interactive')) + .filter((i) => !i.name.endsWith("_interactive")) .map((output) => { const report = analyisRoot?.children ?.find((c) => c.name === output.name + "_analysis") diff --git a/app/routes/jobs.$id._index.tsx b/app/routes/jobs.$id._index.tsx index b5707c4f..c55c9997 100644 --- a/app/routes/jobs.$id._index.tsx +++ b/app/routes/jobs.$id._index.tsx @@ -11,7 +11,7 @@ export const loader = async ({ params, request }: LoaderArgs) => { const user = await getUser(request); const token = await getBartenderTokenByUser(user); const job = await getJobById(jobId, token); - if (job.state ==='ok') { + if (job.state === "ok") { if (user.preferredExpertiseLevel === "easy") { // TODO only redirect when caprieval was run return redirect("report");