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");