From de22a65c43bc1fa4bd8265bbdf59db4515450c31 Mon Sep 17 00:00:00 2001 From: Jung-Choi-01 Date: Sun, 1 Nov 2026 13:56:49 -0500 Subject: [PATCH 01/11] make ProteinTableProteinDto type public --- .../src/components/chromatography/tables/ProteinTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/jbio-app/src/components/chromatography/tables/ProteinTable.tsx b/frontend/jbio-app/src/components/chromatography/tables/ProteinTable.tsx index e5efd822..f0ca1566 100644 --- a/frontend/jbio-app/src/components/chromatography/tables/ProteinTable.tsx +++ b/frontend/jbio-app/src/components/chromatography/tables/ProteinTable.tsx @@ -20,7 +20,7 @@ import { FractionDto, ProteinDto } from '../types' import { Search } from '@mui/icons-material' import { SortDirection } from './Common' -type ProteinTableProteinDto = Pick +export type ProteinTableProteinDto = Pick export type ProteinTableMetricDto

= { label: string From e65660385dfcefdb9bbadf40e1cb55195a0e6bc5 Mon Sep 17 00:00:00 2001 From: Jung-Choi-01 Date: Sun, 1 Nov 2026 13:58:12 -0500 Subject: [PATCH 02/11] parameterize elution line information --- .../chromatography/graphs/Chromatogram.tsx | 24 ++++++++++--------- .../HydrophobicInteractionFractionation.tsx | 2 +- .../src/pages/IonExchangeFractionation.tsx | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx b/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx index 0ed1d873..0273acd9 100644 --- a/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx +++ b/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx @@ -16,10 +16,12 @@ export type ChromatogramProps = { fractionRows: ChromatogramFractionDto[] data: FractionationResponse afterWash?: ProteinDto[] - saltGradient?: Array<{ x: number; y: number }> + elutionPoints?: Array<{ x: number; y: number }> + elutionLabel?: string, + elutionAxisLabel?: string } -export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel, afterWashLabel, fractionRows, data, afterWash, saltGradient }: ChromatogramProps) { +export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel, afterWashLabel, fractionRows, data, afterWash, elutionPoints, elutionLabel = "Salt Gradient", elutionAxisLabel = "Salt Concentration"}: ChromatogramProps) { const lineChartRef = useRef | null>(null) const scatterChartRef = useRef | null>(null) const chartTheme = useChartTheme() @@ -90,10 +92,10 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel, }, ] - if (saltGradient && saltGradient.length > 0) { + if (elutionPoints && elutionPoints.length > 0) { datasets.push({ - label: 'Salt Gradient', - data: saltGradient, + label: elutionLabel, + data: elutionPoints, borderColor: 'rgba(255, 107, 107, 1)', backgroundColor: 'rgba(255, 107, 107, 1)', pointRadius: 0, @@ -105,13 +107,13 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel, yAxisID: 'y1', }) - const saltGradientScale = { + const elutionScale = { y1: { type: 'linear', position: 'right', title: { display: true, - text: 'Salt Concentration', + text: elutionAxisLabel, color: chartTheme.text, }, ticks: { @@ -124,11 +126,11 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel, }, } as const - return { datasets, saltGradientScale } + return { datasets, elutionScale } } return { datasets } - }, [proteinSeriesPoints, saltGradient]) + }, [proteinSeriesPoints, elutionPoints]) const activeChart = showLineGraph ? lineChartRef.current : scatterChartRef.current const downloadChart = useDownloadChart() @@ -186,7 +188,7 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel, color: chartTheme.grid, }, }, - ...(lineData.saltGradientScale ?? {}) + ...(lineData.elutionScale ?? {}) }, }} /> @@ -237,7 +239,7 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel, color: chartTheme.grid, }, }, - ...(lineData.saltGradientScale ?? {}) + ...(lineData.elutionScale ?? {}) }, }} /> diff --git a/frontend/jbio-app/src/pages/HydrophobicInteractionFractionation.tsx b/frontend/jbio-app/src/pages/HydrophobicInteractionFractionation.tsx index ccf5498e..ac07f0b2 100644 --- a/frontend/jbio-app/src/pages/HydrophobicInteractionFractionation.tsx +++ b/frontend/jbio-app/src/pages/HydrophobicInteractionFractionation.tsx @@ -278,7 +278,7 @@ const HydrophobicInteractionFractionation: React.FC = () => { setShowWash(checked)} />} label='Show Wash' sx={{ marginBottom: '0.25rem' }} /> - + diff --git a/frontend/jbio-app/src/pages/IonExchangeFractionation.tsx b/frontend/jbio-app/src/pages/IonExchangeFractionation.tsx index 47172737..96137180 100644 --- a/frontend/jbio-app/src/pages/IonExchangeFractionation.tsx +++ b/frontend/jbio-app/src/pages/IonExchangeFractionation.tsx @@ -199,7 +199,7 @@ const IonExchangeFractionation: React.FC = () => { setUseLogScale(checked)} />} label='Log Scale' sx={{ marginBottom: '0.25rem' }} /> setShowWash(checked)} />} label='Show Wash' sx={{ marginBottom: '0.25rem' }} /> - + From b1ee351e957cebfff0b592ead29d11d13b844fbb Mon Sep 17 00:00:00 2001 From: Jung-Choi-01 Date: Sun, 1 Nov 2026 13:58:33 -0500 Subject: [PATCH 03/11] add TODO notes for zeroth fraction --- .../src/components/chromatography/graphs/Chromatogram.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx b/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx index 0273acd9..f446e09a 100644 --- a/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx +++ b/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx @@ -164,6 +164,7 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel, text: 'Fractions', color: chartTheme.text, }, + // TODO: this always shows zeroth (wash) fraction, either change how wash works or conditionally remove beginAtZero: true, ticks: { callback: fractionTickLabel, @@ -215,6 +216,7 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel, text: 'Fractions', color: chartTheme.text, }, + // TODO: this always shows zeroth (wash) fraction, either change how wash works or conditionally remove beginAtZero: true, ticks: { callback: fractionTickLabel, From 4f3930d1664706d1177c56fe305ebd52ab877a85 Mon Sep 17 00:00:00 2001 From: Jung-Choi-01 Date: Sun, 1 Nov 2026 13:59:00 -0500 Subject: [PATCH 04/11] create affinity chromatography front-end --- .../src/pages/AffinityChromatography.tsx | 354 ++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 frontend/jbio-app/src/pages/AffinityChromatography.tsx diff --git a/frontend/jbio-app/src/pages/AffinityChromatography.tsx b/frontend/jbio-app/src/pages/AffinityChromatography.tsx new file mode 100644 index 00000000..3b72c9b9 --- /dev/null +++ b/frontend/jbio-app/src/pages/AffinityChromatography.tsx @@ -0,0 +1,354 @@ +import { + Alert, + Box, + Button, + Card, + CardContent, + CardHeader, + FormControl, + FormControlLabel, + InputLabel, + MenuItem, + Select, + Slider, + Switch, + Typography, +} from '@mui/material' +import { BarElement, CategoryScale, Chart as ChartJS, Legend, LineElement, LinearScale, LogarithmicScale, PointElement, Tooltip } from 'chart.js' +import React, { useMemo, useState } from 'react' +import { Chromatogram, ChromatogramFractionDto } from '../components/chromatography/graphs/Chromatogram' +import { ChromatographyHitGraph } from '../components/chromatography/graphs/HitGraph' +import { HitTable } from '../components/chromatography/tables/HitTable' +import { ProteinTable, ProteinTableProteinDto } from '../components/chromatography/tables/ProteinTable' +import NumberField from '../components/ui/NumberField' +import { API_URL } from '../config' +import { FractionationResponse } from '../components/chromatography/types' + +/* + * Register the chart components we need for Chart.js. + * Without this, the Line chart and axes will not render properly. + */ +ChartJS.register(CategoryScale, LinearScale, LogarithmicScale, BarElement, LineElement, PointElement, Tooltip, Legend) + +/* + * Protein object returned by the backend for each protein in a fraction. + */ +type ProteinDto = ProteinTableProteinDto & { + id: string + name: string + description: string + sequence: string + molecularWeight: number + amount: number +} + +/* + * One output fraction returned by the backend. + */ +type FractionDto = ChromatogramFractionDto & { + fractionIndex: number + proteinCount?: number + absorbanceScore?: number + hitCount?: number + hitProteinIds?: string[] + proteins: ProteinDto[] +} + +/* + * Parameter metadata returned by the backend for the completed run. + */ +type ParamsDto = { + fractions: number + prosite_strong: string + prosite_weak: string + noise: number +} + +/* + * Summary counts returned by the backend. + */ +type CountsDto = { + total: number + strongly_bound: number + weakly_bound: number + unbound: number + skipped: number +} + +/* + * Fractioon counts returned by the backend. + */ +type FractionCountsDto = { + wash_fractions: number + weak_fractions: number + strong_fractions: number +} + +/* + * Full API response shape for a successful HIC run. + */ +type AffinityResponse = FractionationResponse & { + ok: boolean + params: ParamsDto + counts: CountsDto + fraction_counts: FractionCountsDto + fractions: FractionDto[] + error?: string +} + +type MethodData = Readonly<{ + name: string + prosite_strong: string + prosite_weak: string + elute: string + elute_concentrations: readonly [number, number, number] + elute_concentration_unit: string +}> + +const methods = { + IMAC: { + name: "Immobilized Metal", + prosite_strong: "H-H-H-H-H-H", + prosite_weak: "H-H-H", + elute: "Imidazole", + elute_concentrations: [0, 100, 500], + elute_concentration_unit: "mM" + }, + GST: { + name: "Glutathione", + prosite_strong: "G-S-T-G-S-T", + prosite_weak: "G-S-T", + elute: "Glutathione", + elute_concentrations: [0, 5, 20], + elute_concentration_unit: "mM" + }, + STREP_TAG: { + name: "Strep-tag", + prosite_strong: "W-S-H-P-Q-F-E-K", + prosite_weak: "W-S-H-P", + elute: "Desthiobiotin", + elute_concentrations: [0, 1, 5], + elute_concentration_unit: "mM" + }, + FLAG: { + name: "FLAG", + prosite_strong: "D-Y-K-D-D-D-D-K", + prosite_weak: "D-Y-K-D", + elute: "FLAG peptide", + elute_concentrations: [0, 1, 10], + elute_concentration_unit: "mM" + }, + MBP: { + name: "Maltose-Binding Protein", + prosite_strong: "D-E-E-D", + prosite_weak: "D-E", + elute: "Maltose", + elute_concentrations: [0, 10, 50], + elute_concentration_unit: "mM" + }, +} as const satisfies { [index: string]: MethodData } + +const AffinityChromatography: React.FC = () => { + /* + * Raw FASTA text loaded from the uploaded file. + * This is sent directly to the backend. + */ + const [fastaText, setFastaText] = useState('') + + /** + * User-selected HIC parameters. + */ + const [method, setMethod] = useState(methods.IMAC) + const [methodLastRan, setMethodLastRan] = useState(method); + + /** + * Fractionation controls. + */ + const [fractionCount, setFractionCount] = useState(80) + const [noise, setNoise] = useState(0.1) + + /* + * Stores the full backend response after a successful run. + * The graphs and table are derived from this object. + */ + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + /* + * Stores the full backend response after a successful run. + * The graphs and table are derived from this object. + */ + const [data, setData] = useState(null) + + /* + * Reads an uploaded FASTA file into text and stores it in state. + */ + const handleLoadFasta = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (!file) return + const text = await file.text() + setFastaText(text) + + // Clear the file input so the same file can be uploaded again if needed. + event.target.value = '' + } + + /* + * 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) + setError('') + + try { + const response = await fetch(`${API_URL}/affinity_chromatography/process`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + fasta_content: fastaText, + fraction_count: fractionCount, + prosite_strong: method?.prosite_strong, + prosite_weak: method?.prosite_weak, + noise, + }), + }) + + const json = (await response.json()) as AffinityResponse + if (!response.ok || !json.ok || json.error) { + throw new Error(json.error || 'Failed to process Affinity Chromatography fractionation.') + } + + setData(json) + setMethodLastRan(method) + } catch (err) { + setData(null) + setError(err instanceof Error ? err.message : 'Unexpected error') + } finally { + setLoading(false) + } + } + + // Keep fraction count at least 1 + const handleFractionCountChange = (value: number | null) => { + if (value == null) { + setFractionCount(1) + return + } + + setFractionCount(Math.max(1, Math.floor(value))) + } + + /** + * Generates step-wise elution concentration graph points + * @param data the fractions we're generating points based off of + * @param elution_concentrations the elute concentrations to use for the graph + * @returns array (x,y) representing a step elution gradient for these fractions + */ + function elutionConcentrationPoints( + data: { fractions: Array<{ fractionIndex: number }>, fraction_counts: { wash_fractions: number, weak_fractions: number, strong_fractions: number }}, + elution_concentrations: readonly [number, number, number] + ): Array<{ x: number; y: number }> { + const fractions = data.fractions ?? [] + + return fractions.map((f) => { + return { + x: f.fractionIndex, + y: f.fractionIndex <= data.fraction_counts.wash_fractions ? elution_concentrations[0] + : f.fractionIndex <= data.fraction_counts.weak_fractions + data.fraction_counts.wash_fractions ? elution_concentrations[1] + : elution_concentrations[2] + } + }) + } + + /* + * Convenience alias for the returned fractions. + * Used throughout the graphs and table. + */ + const fractionRows = useMemo(() => data?.fractions ?? [], [data]) + + const elutionGradient = useMemo(() => (data && methodLastRan ? elutionConcentrationPoints(data, methodLastRan.elute_concentrations) : []), [data, methodLastRan]) + + const [showLineGraph, setShowLineGraph] = useState(true) + const [useLogScale, setUseLogScale] = useState(true) + + return ( + + + + + {/* Main control row for user inputs */} + + + Method + + + + + + + + {/* Noise / overlap slider */} + + Noise / Overlap + setNoise(v as number)} valueLabelDisplay='auto' /> + + + {/* Main action buttons */} + + + + + + + {/* Show backend or request errors */} + {error && ( + + {error} + + )} + + + + {data && fractionRows && ( + <> + + + + + setShowLineGraph(checked)} />} label='Line' sx={{ marginBottom: '0.25rem' }} /> + setUseLogScale(checked)} />} label='Log Scale' sx={{ marginBottom: '0.25rem' }} /> + + + {/* TODO: salt gradient name needs to be extracted so we can use our own elute name */} + + + + + Total: {data.counts.total} | Strongly bound: {data.counts.strongly_bound} | Weakly bound: {data.counts.weakly_bound} | Unbound: {data.counts.unbound} | Skipped: {data.counts.skipped} + + + + + + + + + )} + + + ) +} + +export default AffinityChromatography From 56ce652c5356954bef9477cfbaefa53375ec31ad Mon Sep 17 00:00:00 2001 From: Jung-Choi-01 Date: Sun, 1 Nov 2026 13:59:12 -0500 Subject: [PATCH 05/11] show affinity chromatography front-end on main sidebar --- frontend/jbio-app/src/App.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/jbio-app/src/App.tsx b/frontend/jbio-app/src/App.tsx index 7495e774..8ed15e00 100644 --- a/frontend/jbio-app/src/App.tsx +++ b/frontend/jbio-app/src/App.tsx @@ -22,6 +22,7 @@ import PeptideRetention from "./pages/PeptideRetention"; import IonExchangeFractionation from "./pages/IonExchangeFractionation"; import HydrophobicInteractionFractionation from "./pages/HydrophobicInteractionFractionation"; import SizeExclusionPage from './pages/SizeExclusion'; +import AffinityChromatography from './pages/AffinityChromatography'; function App() { const pages = [ @@ -76,6 +77,12 @@ function App() { }, { id: 8, + icon: , + component: , + label: "Affinity Chromatography", + }, + { + id: 9, icon: , link: "https://github.com/paulcraig/ProSep", label: "GitHub Repository", From a75dc40143027c2d5d1f39c134d079eb84b86c67 Mon Sep 17 00:00:00 2001 From: Jung-Choi-01 Date: Sun, 1 Nov 2026 13:59:28 -0500 Subject: [PATCH 06/11] implement affinity chromatography back-end logic --- backend/logic/affinity_chromatography.py | 76 ++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 backend/logic/affinity_chromatography.py diff --git a/backend/logic/affinity_chromatography.py b/backend/logic/affinity_chromatography.py new file mode 100644 index 00000000..306eaa29 --- /dev/null +++ b/backend/logic/affinity_chromatography.py @@ -0,0 +1,76 @@ +from __future__ import annotations +from backend.utility.protein import Protein + +from typing import Any, Dict + +from backend.logic.fractionation.fractionation import Fractionation + +class AffinityChromatography: + """ + Simulates affinity chromatography. + + Main idea: + - Each different ligand in affinity chromatography bonds to different "motifs" at different strengths + - Divide all proteins into either strongly binded, weakly binded, or unbinded + - Wash comes out in first 1/2 fractions + - Weakly binded comes out in next 1/4 fractions + - Strongly bound comes out in next 1/4 fractions + """ + + @staticmethod + def process (fasta_content: str, prosite_strong: str, prosite_weak: str, fraction_count: int = 80, noise: float = 0.10) -> Dict[str, Any]: + """ + Main affinity chromatography simulation function. + + Parameters: + - prosite_strong: the PROSITE motif that matches strongly bound proteins + - prosite_weak: the PROSITE motif that matches weakly bound proteins + - fraction_count: number of output fractions + - noise: overlap between fractions + """ + + if fraction_count < 4: + raise ValueError("fraction_count must be at least 4") + if noise < 0: + raise ValueError("noise must be >= 0") + + # Parse all input FASTA records + entries, skipped = Fractionation.convert_fasta_to_protein_entries(fasta_content) + + # 1. "extract" (filter and remove) all strongly bound proteins + strongly_bound = [entry for entry in entries if Protein.matches_prosite(entry.sequence, prosite_strong)] + unbound = [entry for entry in entries if not Protein.matches_prosite(entry.sequence, prosite_strong)] + + # 2. "extract" (filter and remove) all weakly bound proteins + weakly_bound = [entry for entry in entries if Protein.matches_prosite(entry.sequence, prosite_weak)] + unbound = [entry for entry in unbound if not Protein.matches_prosite(entry.sequence, prosite_weak)] + + # now we have 3 our 3 lists! divide them cool-ly into the fractions + # this is a very convoluted way to get the following ratios: 1/2 1/4 1/4 + wash_fractions = fraction_count // 2 + weak_fractions = (fraction_count - wash_fractions) // 2 + strong_fractions = fraction_count - (wash_fractions + weak_fractions) + + fractions = list(Fractionation.fractionate_with_overlap(unbound, wash_fractions, noise)) + fractions.extend(Fractionation.fractionate_with_overlap(weakly_bound, weak_fractions, noise)) + fractions.extend(Fractionation.fractionate_with_overlap(strongly_bound, strong_fractions, noise)) + + return { + "ok": True, + "params": { + "fractions": fraction_count, + }, + "counts": { + "total": len(entries), + "strongly_bound": len(strongly_bound), + "weakly_bound": len(weakly_bound), + "unbound": len(unbound), + "skipped": skipped, + }, + "fraction_counts": { + "wash_fractions": wash_fractions, + "weak_fractions": weak_fractions, + "strong_fractions": strong_fractions + }, + "fractions": Fractionation.process_fractions(fractions) + } \ No newline at end of file From 43e519e88231a2502063c94b256d7cfc955756c9 Mon Sep 17 00:00:00 2001 From: Jung-Choi-01 Date: Sun, 1 Nov 2026 13:59:42 -0500 Subject: [PATCH 07/11] create route for affinity chromatography --- backend/api/affinity_chromatography_routes.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 backend/api/affinity_chromatography_routes.py diff --git a/backend/api/affinity_chromatography_routes.py b/backend/api/affinity_chromatography_routes.py new file mode 100644 index 00000000..81bab506 --- /dev/null +++ b/backend/api/affinity_chromatography_routes.py @@ -0,0 +1,43 @@ +from typing import Any, Dict + +from fastapi import APIRouter +from pydantic import BaseModel, Field + +from backend.logic.affinity_chromatography import AffinityChromatography + + +router = APIRouter( + prefix="/affinity_chromatography", + tags=["Affinity Chromatography"], +) + +class AffinityChromatographyRequest(BaseModel): + """ + Request body schema for running Affinity Chromatography. + """ + fasta_content: str = Field(..., description="FASTA content as raw text") + prosite_strong: str = Field(..., description="PROSITE motif matching strongly bound proteins") + prosite_weak: str = Field(..., description="PROSITE motif matching weakly bound proteins") + + # Fractionation and simulation controls + fraction_count: int = Field(80, ge=1, le=500) + noise: float = Field(0.10, ge=0.0, le=1.0, description="Overlap between fractions") + +@router.post("/process", response_model=Dict[str, Any]) +async def process_affinity(body: AffinityChromatographyRequest) -> Any: + """ + Run affinity chromatography and return the simulated result. + """ + if not body.fasta_content.strip(): + return {"error": "No FASTA content provided."} + + try: + return AffinityChromatography.process( + fasta_content=body.fasta_content, + fraction_count=body.fraction_count, + prosite_strong=body.prosite_strong, + prosite_weak=body.prosite_weak + ) + except ValueError as exc: + return {"error": str(exc)} + \ No newline at end of file From 3b0c248c1e5967d85064489506e691c3797d20ff Mon Sep 17 00:00:00 2001 From: Jung-Choi-01 Date: Sun, 1 Nov 2026 13:59:52 -0500 Subject: [PATCH 08/11] allow route for affinity chromatography --- backend/server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/server.py b/backend/server.py index 7e196b9d..81369119 100644 --- a/backend/server.py +++ b/backend/server.py @@ -12,6 +12,7 @@ import backend.api.ion_exchange_fractionation_routes as ion_exchange_fractionation_routes import backend.api.size_exclusion as size_exclusion_routes import backend.api.hydrophobic_interaction_fractionation_routes as hydrophobic_interaction_fractionation_routes +import backend.api.affinity_chromatography_routes as affinity_chromatography_routes """ @@ -67,6 +68,7 @@ app.include_router(ion_exchange_fractionation_routes.router) app.include_router(size_exclusion_routes.router) app.include_router(hydrophobic_interaction_fractionation_routes.router) +app.include_router(affinity_chromatography_routes.router) """ NOTE FOR FUTURE DEVELOPERS: From e90728619a2a6dcfea3d0f92b081ac4acc990f80 Mon Sep 17 00:00:00 2001 From: Jung-Choi-01 Date: Sun, 1 Nov 2026 14:00:14 -0500 Subject: [PATCH 09/11] add tentative change to not scale all y values to 1 --- .../components/chromatography/graphs/Chromatogram.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx b/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx index f446e09a..c3a27839 100644 --- a/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx +++ b/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx @@ -46,10 +46,11 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel, if (showAfterWash) { points.push({ x: lastFractionX + 1, y: afterWash.length }) } - const maxY = Math.max(...points.map(p => p.y), 1) - for (const point of points) { - point.y = Math.round((point.y / maxY) * 100) / 100 - } + // const maxY = Math.max(...points.map(p => p.y), 1) + // for (const point of points) { + // point.y = Math.round((point.y / maxY) * 100) / 100 + // } + for(const point of points) point.y += Math.random() return points }, [data, fractionRows, showWash, showAfterWash, afterWash, lastFractionX]) From 9d14ee779348aebec390250c683f844dd15fa408 Mon Sep 17 00:00:00 2001 From: Jung-Choi-01 Date: Sun, 1 Nov 2026 14:39:24 -0500 Subject: [PATCH 10/11] update dependency array to include all dependencies --- .../src/components/chromatography/graphs/Chromatogram.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx b/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx index 28245bec..25803c77 100644 --- a/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx +++ b/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx @@ -131,7 +131,7 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel = } return { datasets } - }, [proteinSeriesPoints, elutionPoints]) + }, [proteinSeriesPoints, elutionPoints, elutionLabel, elutionAxisLabel, chartTheme.text, chartTheme.grid]) const activeChart = showLineGraph ? lineChartRef.current : scatterChartRef.current const downloadChart = useDownloadChart() From ae44770939f3c22f195c46d154469bec3c61e31a Mon Sep 17 00:00:00 2001 From: Jung-Choi-01 Date: Thu, 5 Nov 2026 17:58:55 -0500 Subject: [PATCH 11/11] re-add normalizing, but add super small value to points that are exactly 0 --- .../components/chromatography/graphs/Chromatogram.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx b/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx index 25803c77..c638dfb1 100644 --- a/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx +++ b/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx @@ -46,11 +46,11 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel = if (showAfterWash) { points.push({ x: lastFractionX + 1, y: afterWash.length }) } - // const maxY = Math.max(...points.map(p => p.y), 1) - // for (const point of points) { - // point.y = Math.round((point.y / maxY) * 100) / 100 - // } - for(const point of points) point.y += Math.random() + const maxY = Math.max(...points.map(p => p.y), 1) + for (const point of points) { + point.y = Math.round((point.y / maxY) * 100) / 100 + if(point.y === 0) point.y = 0.001 + } return points }, [data, fractionRows, showWash, showAfterWash, afterWash, lastFractionX])