Skip to content

Commit

Permalink
implement scatter plots
Browse files Browse the repository at this point in the history
Based on report.html generated by haddock3-analyse command
  • Loading branch information
sverhoeven committed Oct 8, 2023
1 parent 76284bb commit 8b06ac7
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 76 deletions.
6 changes: 2 additions & 4 deletions app/components/Haddock3/BoxPlots.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type { Scores } from "./CaprievalReport.client";

export function BoxPlots({ scores }: { scores: Scores }) {
return (
<div>Place holder for boxplots</div>
)
}
return <div>Place holder for boxplots</div>;
}
324 changes: 254 additions & 70 deletions app/components/Haddock3/ScatterPlots.tsx
Original file line number Diff line number Diff line change
@@ -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<br>Score: -13.82<br>Caprieval rank: 16",
text: structures4cluster.map(
(s) =>
`Model: ${s.model}<br>Score: ${s.score}<br>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<br>Score: -13.82<br>Caprieval rank: 16",
text: structures4cluster.map(
(s) =>
`Model: ${s.model}<br>Score: ${s.score}<br>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}<br>${column}: ${cluster[column]}<br>${row}: ${cluster[row]}`,
],
hoverlabel: {
bgcolor: color,
font: { family: "Helvetica", size: 16 },
},
hovertemplate: `<b>Cluster ${cluster.cluster_id}<br>${column}: ${cluster[column]}<br>${row}: ${cluster[row]}</b><extra></extra>`,
legendgroup: name,
mode: "markers",
showlegend: false,
type: "scatter",
xaxis,
yaxis,
});
} else {
// Fill other cluster
other.x = (other.x as Array<string | number>).concat(
structures4cluster.map((s) => s[row])
);
other.y = (other.y as Array<string | number>).concat(
structures4cluster.map((s) => s[column])
);
other.text = (other.text as Array<string>).concat(
structures4cluster.map(
(s) =>
`Model: ${s.model}<br>Score: ${s.score}<br>Caprieval rank: ${s.caprieval_rank}`
)
);
}
}
if (other.x!.length > 0) {
data.push(other);
}
}

function generateAxes() {
const axes: Record<string, Partial<LayoutAxis & { matches: string }>> = {};
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<string, Partial<LayoutAxis & { matches: string }>>
) {
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<Layout> = {
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 <Plot data={data} layout={layout} />;
return (
<Plot
data={data}
layout={layout}
config={{
responsive: true,
}}
/>
);
}
2 changes: 1 addition & 1 deletion app/components/OutputReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion app/routes/jobs.$id._index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down

0 comments on commit 8b06ac7

Please sign in to comment.