Skip to content
Merged
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
2 changes: 2 additions & 0 deletions frontend/jbio-app/src/Theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ let darkThemeBase = createTheme({
text: {
primary: '#ffffff',
secondary: '#ffffff',
disabled: '#999999'
},
primary: {
main: '#8ea2ff',
Expand All @@ -35,6 +36,7 @@ let lightThemeBase = createTheme({
text: {
primary: '#000000',
secondary: '#000000',
disabled: '#999999'
},
primary: {
main: '#4f6edb',
Expand Down
69 changes: 69 additions & 0 deletions frontend/jbio-app/src/components/Timer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Timer } from "@mui/icons-material"
import { Box } from "@mui/material"
import { useRef, useState } from "react"

function formatTime(timeInSeconds: number) {
const minutes = Math.floor(timeInSeconds / 60)
const secondsRemainder = timeInSeconds % 60
const secondsRemainderFormatted = secondsRemainder.toString().padStart(2, '0')
return minutes > 0 ? `${minutes}m ${secondsRemainderFormatted}s` : `${secondsRemainderFormatted}s`
}

export function useTimer() {
const [timeInSeconds, setTimeInSeconds] = useState(0)
const [isTimerRunning, setIsTimerRunning] = useState(false)
const [hasTimerRanYet, setHasTimerRanYet] = useState(false)
const timerRef = useRef<NodeJS.Timeout | null>(null)

function startTimer() {
if (timerRef.current) clearInterval(timerRef.current)

timerRef.current = setInterval(() => {
setTimeInSeconds((prev) => prev + 1)
}, 1000)

setIsTimerRunning(true)
setHasTimerRanYet(true)
}

function stopTimer() {
if (timerRef.current) {
clearInterval(timerRef.current)
timerRef.current = null
}

setIsTimerRunning(false)
}

return {
startTimer,
stopTimer,
setTimeInSeconds,
isTimerRunning,
hasTimerRanYet,
timeInSeconds
}
}

export type StopwatchProps = {
timeInSeconds: number
isTimerRunning: boolean
hasTimerRanYet: boolean
}
export function Stopwatch({ timeInSeconds, isTimerRunning, hasTimerRanYet }: StopwatchProps) {
return hasTimerRanYet ? (
<Box
sx={{
display: 'flex',
alignItems: 'center',
fontWeight: 'bold',
}}
color={isTimerRunning ? 'text' : 'text.disabled'}
>
<Timer fontSize='small' style={{ marginRight: '0.25rem' }} />
{formatTime(timeInSeconds)}
</Box>
): (
<></>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { HitTable } from '../components/chromatography/tables/HitTable'
import { ProteinTable } from '../components/chromatography/tables/ProteinTable'
import { Chromatogram } from '../components/chromatography/graphs/Chromatogram'
import { ChromatographyHitGraph } from '../components/chromatography/graphs/HitGraph'
import { Stopwatch, useTimer } from '../components/Timer'

/*
* Register the chart components we need for Chart.js.
Expand Down Expand Up @@ -145,12 +146,16 @@ const HydrophobicInteractionFractionation: React.FC = () => {
event.target.value = ''
}

const timer = useTimer()

/*
* Sends the current form values and FASTA content to the backend.
* On success, stores the returned HIC result in `data`.
*/
const handleRun = async () => {
setLoading(true)
timer.setTimeInSeconds(0)
timer.startTimer()
setError('')

try {
Expand Down Expand Up @@ -180,6 +185,7 @@ const HydrophobicInteractionFractionation: React.FC = () => {
setError(err instanceof Error ? err.message : 'Unexpected error')
} finally {
setLoading(false)
timer.stopTimer()
}
}

Expand Down Expand Up @@ -314,6 +320,7 @@ const HydrophobicInteractionFractionation: React.FC = () => {
<Button variant='contained' onClick={handleRun} disabled={loading || !fastaText.trim()}>
{loading ? 'Running...' : 'Run Fractionation'}
</Button>
<Stopwatch {...timer} />
</Box>

{/* Show backend or request errors */}
Expand Down
7 changes: 7 additions & 0 deletions frontend/jbio-app/src/pages/IonExchangeFractionation.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo, useRef, useState } from 'react'

Check warning on line 1 in frontend/jbio-app/src/pages/IonExchangeFractionation.tsx

View workflow job for this annotation

GitHub Actions / ESLint + Prettier

'useRef' is defined but never used
import {
Alert,
Box,
Expand All @@ -23,6 +23,7 @@
import { Chromatogram } from '../components/chromatography/graphs/Chromatogram'
import { ChromatographyHitGraph } from '../components/chromatography/graphs/HitGraph'
import type * as ChromatographyTypes from '../components/chromatography/types'
import { Stopwatch, useTimer } from '../components/Timer'

ChartJS.register(CategoryScale, LinearScale, LogarithmicScale, BarElement, LineElement, PointElement, Tooltip, Legend)

Expand Down Expand Up @@ -76,6 +77,8 @@
const [useLogScale, setUseLogScale] = useState<boolean>(true)
const [showWash, setShowWash] = useState<boolean>(false)

const timer = useTimer()

const handleLoadFasta = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) {
Expand All @@ -90,6 +93,8 @@
const handleRunFractionation = async () => {
setLoading(true)
setError('')
timer.setTimeInSeconds(0)
timer.startTimer()

try {
const response = await fetch(`${API_URL}/ion_exchange_fractionation/process`, {
Expand All @@ -116,6 +121,7 @@
setError(err instanceof Error ? err.message : 'Unexpected error')
} finally {
setLoading(false)
timer.stopTimer()
}
}

Expand Down Expand Up @@ -162,6 +168,7 @@
<Button variant='contained' disabled={loading || fastaText.trim().length === 0} onClick={handleRunFractionation}>
{loading ? 'Processing...' : 'Run Fractionation'}
</Button>
<Stopwatch {...timer} />
</Box>

{error && (
Expand Down
111 changes: 50 additions & 61 deletions frontend/jbio-app/src/pages/PeptideRetention.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
styled,
tooltipClasses,
TooltipProps,
Icon,

Check warning on line 24 in frontend/jbio-app/src/pages/PeptideRetention.tsx

View workflow job for this annotation

GitHub Actions / ESLint + Prettier

'Icon' is defined but never used
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import ContentCopy from "@mui/icons-material/ContentCopy";
Expand All @@ -35,7 +35,7 @@
Title,
Legend,
} from "chart.js";
import zoomPlugin, { zoom } from "chartjs-plugin-zoom";

Check warning on line 38 in frontend/jbio-app/src/pages/PeptideRetention.tsx

View workflow job for this annotation

GitHub Actions / ESLint + Prettier

'zoom' is defined but never used
import annotationPlugin from "chartjs-plugin-annotation";
import { API_URL } from "../config";
import "./PeptideRetention.css";
Expand All @@ -45,7 +45,7 @@
DownloadOutlined,
FileOpenOutlined,
StopCircle,
Timer,

Check warning on line 48 in frontend/jbio-app/src/pages/PeptideRetention.tsx

View workflow job for this annotation

GitHub Actions / ESLint + Prettier

'Timer' is defined but never used
Info,
ZoomOutOutlined,
RestartAlt,
Expand All @@ -53,6 +53,7 @@
} from "@mui/icons-material";
import { ImageHoverPreview } from "../components/ImageHoverPreview";
import { useLocation } from "react-router-dom";
import { Stopwatch, useTimer } from "../components/Timer"

ChartJS.register(
CategoryScale,
Expand Down Expand Up @@ -103,14 +104,13 @@
const [results, setResults] = useState<PredictionResult[]>([]);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [stopRequested, setStopRequested] = useState(false);
const [elapsed, setElapsed] = useState(0);
const [useCached, setUseCached] = useState(true);

const timerRef = useRef<NodeJS.Timeout | null>(null);
const abortRef = useRef<AbortController | null>(null);
const chartRef = useRef<any>(null);

const [colors, setColors] = useState({

Check warning on line 113 in frontend/jbio-app/src/pages/PeptideRetention.tsx

View workflow job for this annotation

GitHub Actions / ESLint + Prettier

'setColors' is assigned a value but never used
text: "#000000",
background: "#ffffff",
line: "#000000",
Expand Down Expand Up @@ -256,22 +256,19 @@
}
};

const timer = useTimer()
const predictAll = async () => {
if (!peptides.length) return;

setIsLoading(true);
setStopRequested(false);
setErrorMessage(null);
setResults([]);
setElapsed(0);
timer.setTimeInSeconds(0)
timer.startTimer()

abortRef.current = new AbortController();

if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(() => {
setElapsed((prev) => prev + 1);
}, 1000);

try {
const cache = getCache();
let peptidesToPredict = peptides;
Expand Down Expand Up @@ -305,21 +302,10 @@
if (err.name !== "AbortError") setErrorMessage(`Error: ${err.message}`);
}

if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
timer.stopTimer()
setIsLoading(false);
};

const formatTime = (totalSec: number) => {
const mins = Math.floor(totalSec / 60);
const secs = totalSec % 60;
return secs > 0
? `${mins}m ${secs.toString().padStart(2, "0")}s`
: `${secs}s`;
};

const exportCsv = () => {
let csv = "Peptide,Predicted tR (min),SMILES,log SumAA,log VDW Vol,clogP\n";
results.forEach((result) => {
Expand Down Expand Up @@ -372,33 +358,39 @@
setResults([]);
};

const generateChromatogramData = (textColor: string) => {
const scalingFactor = 6.5;
const numPoints = 1000;
const xVals = Array.from(
{ length: numPoints },
(_, i) => (i / (numPoints - 1)) * 100,
);
const generateChromatogramData = (textColor: string) => {
// Roughly equivalent to previous sigma value, but in terms of minutes
const sigma = 0.054;

// Roughly equivalent to previous step value, but in terms of minutes
const step = sigma / 4;

const valid_results = results.filter((result): result is PredictionSuccess => !("error" in result));

let longest_tr = 0;
if (valid_results.length) {longest_tr = Math.max(...valid_results.map((result) => result.predicted_tr));}

// Max x value in mins, 10% larger than the maximum value in results (for a bit of padding)
// Min val of 10 minutes for x_max
const x_max = Math.max(10, longest_tr * 1.10);

// Number of points on the grid
const numPoints = Math.ceil(x_max/step) + 1;

const chromatogram = new Array(numPoints).fill(0);
const noise = Array.from({ length: numPoints }, () => Math.random());
const annotations: any = {};
let maxChrom = 0;

const step = 100 / (numPoints - 1);
const sigma = 0.35;
const sigmaSq2 = 2 * sigma ** 2;
const rangeDist = 5 * sigma;
const rangeIdx = Math.ceil(rangeDist / step);

results.forEach((result, index) => {
if ("error" in result) return;

valid_results.forEach((result, index) => {
const peptide = result.peptide.replace(/-NH2/g, "").replace(/Ac-/g, "");
const aaCount = peptide.length;
const rt = result.predicted_tr * scalingFactor;
const height = aaCount * 10;

// x axis now properly scales to mins, so no scaling needed here for predicted_tr
const rt = result.predicted_tr;
const height = peptide.length * 10;

const centerIdx = Math.round((rt / 100) * (numPoints - 1));
const centerIdx = Math.round((rt / step));
const startIdx = Math.max(0, centerIdx - rangeIdx);
const endIdx = Math.min(numPoints - 1, centerIdx + rangeIdx);

Expand All @@ -409,7 +401,7 @@

annotations[`peak-label-${index}`] = {
type: "label",
xValue: rt * 10 - 0.5,
xValue: rt,
yValue: height + peptide.length + 10,
content: peptide,
font: { size: 13, weight: "bold", color: textColor },
Expand All @@ -424,7 +416,7 @@

annotations[`peak-point-${index}`] = {
type: "point",
xValue: rt * 10 - 0.5,
xValue: rt,
yValue: height,
radius: 6,
backgroundColor: "#1976d2",
Expand All @@ -433,24 +425,32 @@
};
});

chromatogram.forEach((val, i) => {
chromatogram[i] = Math.max(0, val + noise[i]);
if (chromatogram[i] > maxChrom) maxChrom = chromatogram[i];
// chromatogram.forEach((val, i) => {
// chromatogram[i] = Math.max(0, val + Math.random[i]);
// if (chromatogram[i] > maxChrom) maxChrom = chromatogram[i];
// });

let maxChrom = 0;
const chromatogram_points = chromatogram.map((y, i) => {
const val = Math.max(0, y + Math.random()); // + some noise
if (val > maxChrom) maxChrom = val;
return {x: i * step, y: val};
});

return {
labels: xVals,
//labels: xVals,
datasets: [
{
label: "Chromatogram",
data: chromatogram,
data: chromatogram_points,
borderColor: results.length === 0 ? "transparent" : colors.line,
borderWidth: 2,
backgroundColor: colors.background,
fill: true,
},
],
max: maxChrom,
x_max,
annotations,
};
};
Expand Down Expand Up @@ -622,20 +622,7 @@
<div style={{ display: "flex", alignItems: "center", gap: "20px" }}>
{(isLoading || results.length > 0) && (
<>
<div /* Timer */
style={{
display: "flex",
alignItems: "center",
fontWeight: "bold",
color: "var(--sub-text)",
}}
>
<Timer
fontSize="small"
style={{ marginRight: "0.25rem", fill: "var(--text)" }}
/>
{formatTime(elapsed)}
</div>
<Stopwatch {...timer} />

<div /* Progress */
style={{
Expand Down Expand Up @@ -897,6 +884,7 @@
mode: "x",
},
pan: { enabled: true, mode: "x" },
limits: { x: { min: 0, max: chromatogramData.x_max,minRange: 0.5 } },
},
},
elements: {
Expand All @@ -905,8 +893,9 @@
},
scales: {
x: {
type: "linear",
min: 0,
max: 100,
max: chromatogramData.x_max,
title: {
display: true,
text: "Retention Time (min)",
Expand All @@ -915,7 +904,7 @@
ticks: {
maxTicksLimit: 7,
callback: (value) =>
(Number(value) / 65).toFixed(1),
(Number(value)).toFixed(1),
color: colors.text,
},
},
Expand Down
Loading
Loading