From b4f91ef9e688bbb1cf5aa2796664f4bfb1157a14 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 8 Oct 2026 19:06:18 +0000 Subject: [PATCH 1/4] x-axis/zoom fix for chromatogram --- .../jbio-app/src/pages/PeptideRetention.tsx | 72 +++++++++++-------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/frontend/jbio-app/src/pages/PeptideRetention.tsx b/frontend/jbio-app/src/pages/PeptideRetention.tsx index 6840c601..02f2d5fe 100644 --- a/frontend/jbio-app/src/pages/PeptideRetention.tsx +++ b/frontend/jbio-app/src/pages/PeptideRetention.tsx @@ -372,33 +372,39 @@ const PeptideRetention: React.FC = () => { 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); @@ -409,7 +415,7 @@ const PeptideRetention: React.FC = () => { 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 }, @@ -424,7 +430,7 @@ const PeptideRetention: React.FC = () => { annotations[`peak-point-${index}`] = { type: "point", - xValue: rt * 10 - 0.5, + xValue: rt, yValue: height, radius: 6, backgroundColor: "#1976d2", @@ -433,17 +439,24 @@ const PeptideRetention: React.FC = () => { }; }); - 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, @@ -451,6 +464,7 @@ const PeptideRetention: React.FC = () => { }, ], max: maxChrom, + x_max, annotations, }; }; @@ -897,6 +911,7 @@ const PeptideRetention: React.FC = () => { mode: "x", }, pan: { enabled: true, mode: "x" }, + limits: { x: { min: 0, max: chromatogramData.x_max,minRange: 0.5 } }, }, }, elements: { @@ -905,8 +920,9 @@ const PeptideRetention: React.FC = () => { }, scales: { x: { + type: "linear", min: 0, - max: 100, + max: chromatogramData.x_max, title: { display: true, text: "Retention Time (min)", @@ -915,7 +931,7 @@ const PeptideRetention: React.FC = () => { ticks: { maxTicksLimit: 7, callback: (value) => - (Number(value) / 65).toFixed(1), + (Number(value)).toFixed(1), color: colors.text, }, }, From ad6e8faf3eb7d6b347879618df41daa981e724db Mon Sep 17 00:00:00 2001 From: Scapsters Date: Wed, 21 Oct 2026 12:27:42 -0400 Subject: [PATCH 2/4] add timer, replace with timer in peptide retention --- frontend/jbio-app/src/Theme.tsx | 2 + frontend/jbio-app/src/components/Timer.tsx | 63 +++++++++++++++++++ .../HydrophobicInteractionFractionation.tsx | 7 +++ .../src/pages/IonExchangeFractionation.tsx | 7 +++ .../jbio-app/src/pages/PeptideRetention.tsx | 39 ++---------- frontend/jbio-app/src/pages/SizeExclusion.tsx | 9 ++- 6 files changed, 93 insertions(+), 34 deletions(-) create mode 100644 frontend/jbio-app/src/components/Timer.tsx diff --git a/frontend/jbio-app/src/Theme.tsx b/frontend/jbio-app/src/Theme.tsx index a221ae8f..1e71f63f 100644 --- a/frontend/jbio-app/src/Theme.tsx +++ b/frontend/jbio-app/src/Theme.tsx @@ -10,6 +10,7 @@ let darkThemeBase = createTheme({ text: { primary: '#ffffff', secondary: '#ffffff', + disabled: '#999999' }, primary: { main: '#8ea2ff', @@ -35,6 +36,7 @@ let lightThemeBase = createTheme({ text: { primary: '#000000', secondary: '#000000', + disabled: '#999999' }, primary: { main: '#4f6edb', diff --git a/frontend/jbio-app/src/components/Timer.tsx b/frontend/jbio-app/src/components/Timer.tsx new file mode 100644 index 00000000..ca6f163f --- /dev/null +++ b/frontend/jbio-app/src/components/Timer.tsx @@ -0,0 +1,63 @@ +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 timerRef = useRef(null) + + function startTimer() { + if (timerRef.current) clearInterval(timerRef.current) + + timerRef.current = setInterval(() => { + setTimeInSeconds((prev) => prev + 1) + }, 1000) + + setIsTimerRunning(true) + } + + function stopTimer() { + if (timerRef.current) { + clearInterval(timerRef.current) + timerRef.current = null + } + + setIsTimerRunning(false) + } + + return { + startTimer, + stopTimer, + setTimeInSeconds, + isTimerRunning, + timeInSeconds + } +} + +export type StopwatchProps = { + timeInSeconds: number + isTimerRunning: boolean +} +export function Stopwatch({ timeInSeconds, isTimerRunning }: StopwatchProps) { + return ( + + + {formatTime(timeInSeconds)} + + ) +} diff --git a/frontend/jbio-app/src/pages/HydrophobicInteractionFractionation.tsx b/frontend/jbio-app/src/pages/HydrophobicInteractionFractionation.tsx index 5cd7626a..edea4c80 100644 --- a/frontend/jbio-app/src/pages/HydrophobicInteractionFractionation.tsx +++ b/frontend/jbio-app/src/pages/HydrophobicInteractionFractionation.tsx @@ -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. @@ -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 { @@ -180,6 +185,7 @@ const HydrophobicInteractionFractionation: React.FC = () => { setError(err instanceof Error ? err.message : 'Unexpected error') } finally { setLoading(false) + timer.stopTimer() } } @@ -314,6 +320,7 @@ const HydrophobicInteractionFractionation: React.FC = () => { + {/* Show backend or request errors */} diff --git a/frontend/jbio-app/src/pages/IonExchangeFractionation.tsx b/frontend/jbio-app/src/pages/IonExchangeFractionation.tsx index ff79e395..20cab78c 100644 --- a/frontend/jbio-app/src/pages/IonExchangeFractionation.tsx +++ b/frontend/jbio-app/src/pages/IonExchangeFractionation.tsx @@ -23,6 +23,7 @@ import { ProteinTable } from '../components/chromatography/tables/ProteinTable' 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) @@ -76,6 +77,8 @@ const IonExchangeFractionation: React.FC = () => { const [useLogScale, setUseLogScale] = useState(true) const [showWash, setShowWash] = useState(false) + const timer = useTimer() + const handleLoadFasta = async (event: React.ChangeEvent) => { const file = event.target.files?.[0] if (!file) { @@ -90,6 +93,8 @@ const IonExchangeFractionation: React.FC = () => { const handleRunFractionation = async () => { setLoading(true) setError('') + timer.setTimeInSeconds(0) + timer.startTimer() try { const response = await fetch(`${API_URL}/ion_exchange_fractionation/process`, { @@ -116,6 +121,7 @@ const IonExchangeFractionation: React.FC = () => { setError(err instanceof Error ? err.message : 'Unexpected error') } finally { setLoading(false) + timer.stopTimer() } } @@ -162,6 +168,7 @@ const IonExchangeFractionation: React.FC = () => { + {error && ( diff --git a/frontend/jbio-app/src/pages/PeptideRetention.tsx b/frontend/jbio-app/src/pages/PeptideRetention.tsx index 9d08b5f7..4fd2f412 100644 --- a/frontend/jbio-app/src/pages/PeptideRetention.tsx +++ b/frontend/jbio-app/src/pages/PeptideRetention.tsx @@ -53,6 +53,7 @@ import { } from "@mui/icons-material"; import { ImageHoverPreview } from "../components/ImageHoverPreview"; import { useLocation } from "react-router-dom"; +import { Stopwatch, useTimer } from "../components/Timer" ChartJS.register( CategoryScale, @@ -103,7 +104,6 @@ const PeptideRetention: React.FC = () => { const [results, setResults] = useState([]); const [errorMessage, setErrorMessage] = useState(null); const [stopRequested, setStopRequested] = useState(false); - const [elapsed, setElapsed] = useState(0); const [useCached, setUseCached] = useState(true); const timerRef = useRef(null); @@ -256,6 +256,7 @@ const PeptideRetention: React.FC = () => { } }; + const timer = useTimer() const predictAll = async () => { if (!peptides.length) return; @@ -263,15 +264,11 @@ const PeptideRetention: React.FC = () => { 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; @@ -305,21 +302,10 @@ const PeptideRetention: React.FC = () => { 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) => { @@ -622,20 +608,7 @@ const PeptideRetention: React.FC = () => {
{(isLoading || results.length > 0) && ( <> -
- - {formatTime(elapsed)} -
+
{ setMaxSize(gelDict[gel][1]) } + const timer = useTimer() + const handleRun = async () => { setLoading(true) + timer.setTimeInSeconds(0) + timer.startTimer() setError('') try { const res = await fetch(`${API_URL}/size_exclusion/process`, { @@ -135,6 +140,7 @@ const SizeExclusionPage: React.FC = () => { setData(null) } finally { setLoading(false) + timer.stopTimer() } } @@ -162,7 +168,7 @@ const SizeExclusionPage: React.FC = () => { setFractionCount(value == null ? 1 : Math.max(1, Math.floor(value)))} /> - + + From c6c5e37b512d5fb38ae60e92b09bbeb7a963c014 Mon Sep 17 00:00:00 2001 From: Scapsters Date: Thu, 22 Oct 2026 16:59:52 -0400 Subject: [PATCH 3/4] make spacing between buttons same on fractinations --- frontend/jbio-app/src/pages/SizeExclusion.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/jbio-app/src/pages/SizeExclusion.tsx b/frontend/jbio-app/src/pages/SizeExclusion.tsx index 508823a1..5e87d955 100644 --- a/frontend/jbio-app/src/pages/SizeExclusion.tsx +++ b/frontend/jbio-app/src/pages/SizeExclusion.tsx @@ -174,7 +174,7 @@ const SizeExclusionPage: React.FC = () => { - From 48fb390a80ea3795d6484fee65fa065f132e462c Mon Sep 17 00:00:00 2001 From: Scapsters Date: Thu, 22 Oct 2026 17:28:54 -0400 Subject: [PATCH 4/4] make timer invisible until it first starts --- frontend/jbio-app/src/components/Timer.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/jbio-app/src/components/Timer.tsx b/frontend/jbio-app/src/components/Timer.tsx index ca6f163f..db0820b8 100644 --- a/frontend/jbio-app/src/components/Timer.tsx +++ b/frontend/jbio-app/src/components/Timer.tsx @@ -10,8 +10,9 @@ function formatTime(timeInSeconds: number) { } export function useTimer() { - const [timeInSeconds, setTimeInSeconds] = useState(0); + const [timeInSeconds, setTimeInSeconds] = useState(0) const [isTimerRunning, setIsTimerRunning] = useState(false) + const [hasTimerRanYet, setHasTimerRanYet] = useState(false) const timerRef = useRef(null) function startTimer() { @@ -22,6 +23,7 @@ export function useTimer() { }, 1000) setIsTimerRunning(true) + setHasTimerRanYet(true) } function stopTimer() { @@ -38,6 +40,7 @@ export function useTimer() { stopTimer, setTimeInSeconds, isTimerRunning, + hasTimerRanYet, timeInSeconds } } @@ -45,9 +48,10 @@ export function useTimer() { export type StopwatchProps = { timeInSeconds: number isTimerRunning: boolean + hasTimerRanYet: boolean } -export function Stopwatch({ timeInSeconds, isTimerRunning }: StopwatchProps) { - return ( +export function Stopwatch({ timeInSeconds, isTimerRunning, hasTimerRanYet }: StopwatchProps) { + return hasTimerRanYet ? ( {formatTime(timeInSeconds)} + ): ( + <> ) }