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 diff --git a/backend/logic/affinity_chromatography.py b/backend/logic/affinity_chromatography.py new file mode 100644 index 00000000..e6ab2d83 --- /dev/null +++ b/backend/logic/affinity_chromatography.py @@ -0,0 +1,78 @@ +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) + + offset = 0.025 # offset fraction distributions by 1/40th of the fractions + + fractions = list(Fractionation.fractionate_with_inverse_gaussian(unbound, wash_fractions, offset=offset*2)) + fractions.extend(Fractionation.fractionate_with_inverse_gaussian(weakly_bound, weak_fractions, offset=offset*4)) + fractions.extend(Fractionation.fractionate_with_spike(strongly_bound, strong_fractions, offset=offset*4)) + + 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 diff --git a/backend/logic/fractionation/fractionation.py b/backend/logic/fractionation/fractionation.py index 6a4ba449..afee2f05 100644 --- a/backend/logic/fractionation/fractionation.py +++ b/backend/logic/fractionation/fractionation.py @@ -11,7 +11,6 @@ from backend.utility.protein import Protein - class Fractionation: @dataclass class ProteinEntry: @@ -120,6 +119,96 @@ def fractionate_with_overlap[T: ProteinEntry]( fractions.append(items[start:end]) return fractions + + @staticmethod + def fractionate_with_inverse_gaussian[T: ProteinEntry]( + items: Sequence[T], + n_fractions: int, + mean: float = 0.3, + shape: float = 0.4, + x_range: float = 3, + offset: float = 0.025, + random: float = 0.003 + ) -> Sequence[Sequence[T]]: + """ + Split retained proteins into fractions following an inverse gaussian distribution + Here's a cool desmos graph showing how it works https://www.desmos.com/calculator/n8c4ucn4bv + + Args: + items: proteins to fractionate + n_fractions: number of fractions, n_fractions > 0 + mean: mean to use in formula, mean > 0 + steepness: shape parameter to use in formula, steepness > 0 + x_range: the range of x values to scale to our fraction range, x_range > 0 + offset: the offset for the start of the distribution, 0 <= offset <= 1 + random: the randomness to apply to the weights, 0 <= random, but i recommend random <= 0.01 + """ + + if n_fractions <= 0: + return [] + if not items: + return [[] for _ in range(n_fractions)] + + # create n_fractions points along an inverse gaussian distribution + weights = [] + for index in range(n_fractions): + x_percent = (index + 0.5) / n_fractions - offset + x_percent = max(x_percent, 0.001) + x = x_percent * x_range + # formula from https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution + y = (math.sqrt(shape / (2 * math.pi * x**3)) * + math.exp(-(shape * (x - mean) ** 2) / (2 * mean**2 * x))) + random + weights.append(y) + + # build a list of fraction_sizes based on the % of point over whole + weight_total = sum(weights) + expected_sizes = [len(items) * weight / weight_total for weight in weights] + fraction_sizes = [math.floor(size) for size in expected_sizes] + + # add remainders into fractions in order of descending remainder + # e.g. expected_sizes = [2.4, 5.7, 1.9] + # fraction_sizes = [2, 5, 1], fraction_remainders = [0.4, 0.7, 0.9] + # we need to distribute 2 remaining, so put them into 0.9 and then 0.7 for final counts of 2, 6, 2 + remaining = len(items) - sum(fraction_sizes) + fraction_remainders = [] + for index, expected_size in enumerate(expected_sizes): + remainder = expected_size - math.floor(expected_size) + fraction_remainders.append((remainder, index)) + + fraction_remainders.sort(reverse=True) + + for remainder, fraction_index in fraction_remainders[:remaining]: + fraction_sizes[fraction_index] += 1 + + # finally, now that we have our counts, build the result array! + fractions = [] + start = 0 + for size in fraction_sizes: + fractions.append(items[start:start + size]) + start += size + + return fractions + + @staticmethod + def fractionate_with_spike[T: ProteinEntry]( + items: Sequence[T], + n_fractions: int, + offset: float = 0.025 + ) -> Sequence[Sequence[T]]: + """ + literally puts all proteins in items into a spike at offset% of the fractions + """ + if n_fractions <= 0: + return [] + if not items: + return [[] for _ in range(n_fractions)] + + spike_index = math.floor(offset*n_fractions) + fractions: list[Sequence[T]] = [[] for _ in range(0, spike_index)] + fractions.append(items) + fractions.extend([] for _ in range(spike_index+1, n_fractions)) + + return fractions @staticmethod def process_fractions[T: ProteinEntry](fraction_lists: Sequence[Sequence[T]]): 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: diff --git a/frontend/jbio-app/src/App.tsx b/frontend/jbio-app/src/App.tsx index 47123ab9..4ebb465a 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", diff --git a/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx b/frontend/jbio-app/src/components/chromatography/graphs/Chromatogram.tsx index a6bdb4dc..c638dfb1 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 = 'Wash', afterWashLabel = 'After Wash', fractionRows, data, afterWash, saltGradient }: ChromatogramProps) { +export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel = 'Wash', afterWashLabel = 'After Wash', fractionRows, data, afterWash, elutionPoints, elutionLabel = "Salt Gradient", elutionAxisLabel = "Salt Concentration"}: ChromatogramProps) { const lineChartRef = useRef | null>(null) const scatterChartRef = useRef | null>(null) const chartTheme = useChartTheme() @@ -47,6 +49,7 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel = 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]) @@ -90,10 +93,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 +108,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 +127,11 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel = }, } as const - return { datasets, saltGradientScale } + return { datasets, elutionScale } } return { datasets } - }, [proteinSeriesPoints, saltGradient]) + }, [proteinSeriesPoints, elutionPoints, elutionLabel, elutionAxisLabel, chartTheme.text, chartTheme.grid]) const activeChart = showLineGraph ? lineChartRef.current : scatterChartRef.current const downloadChart = useDownloadChart() @@ -162,6 +165,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, @@ -186,7 +190,7 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel = color: chartTheme.grid, }, }, - ...(lineData.saltGradientScale ?? {}) + ...(lineData.elutionScale ?? {}) }, }} /> @@ -213,6 +217,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, @@ -237,7 +242,7 @@ export function Chromatogram({ showLineGraph, useLogScale, showWash, washLabel = color: chartTheme.grid, }, }, - ...(lineData.saltGradientScale ?? {}) + ...(lineData.elutionScale ?? {}) }, }} /> diff --git a/frontend/jbio-app/src/components/chromatography/tables/ProteinTable.tsx b/frontend/jbio-app/src/components/chromatography/tables/ProteinTable.tsx index e5efd822..a9adfabe 100644 --- a/frontend/jbio-app/src/components/chromatography/tables/ProteinTable.tsx +++ b/frontend/jbio-app/src/components/chromatography/tables/ProteinTable.tsx @@ -15,12 +15,12 @@ import { TextField, Typography, } from '@mui/material' -import { useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' 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 @@ -77,13 +77,13 @@ export function ProteinTable

({ fractionRows, m }) }, [retainedRows, proteinSearch]) - const getSortValue = (row: P, key: string) => { + const getSortValue = useCallback((row: P, key: string) => { if (key === 'molecularWeight') { return row.molecularWeight } const metric = metrics.find(m => m.label === key) return metric ? metric.value(row) : 0 - } + }, [metrics]) const sortedRetainedRows = useMemo(() => { const rows = [...filteredRetainedRows] @@ -94,7 +94,7 @@ export function ProteinTable

({ fractionRows, m }) return rows - }, [filteredRetainedRows, proteinSort, metrics]) + }, [filteredRetainedRows, proteinSort, getSortValue]) const pagedProteins = useMemo(() => { const start = proteinPage * proteinRowsPerPage diff --git a/frontend/jbio-app/src/pages/AffinityChromatography.tsx b/frontend/jbio-app/src/pages/AffinityChromatography.tsx new file mode 100644 index 00000000..d6074452 --- /dev/null +++ b/frontend/jbio-app/src/pages/AffinityChromatography.tsx @@ -0,0 +1,361 @@ +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" + }, + AVITAG_BIOTIN: { + name: "Biotinylated AviTag", + prosite_strong: "G-L-N-D-I-F-E-A-Q-K-I-E-W-H-E", + prosite_weak: "G-L-N-D-I-F-E-A-Q-K", + elute: "Biotin", + elute_concentrations: [0, 1, 10], + 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' }} /> + + + + + + + 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 diff --git a/frontend/jbio-app/src/pages/HydrophobicInteractionFractionation.tsx b/frontend/jbio-app/src/pages/HydrophobicInteractionFractionation.tsx index 1c8aaded..61d3a690 100644 --- a/frontend/jbio-app/src/pages/HydrophobicInteractionFractionation.tsx +++ b/frontend/jbio-app/src/pages/HydrophobicInteractionFractionation.tsx @@ -191,36 +191,6 @@ const HydrophobicInteractionFractionation: React.FC = () => { } } - // Keep salt start non-negative, but do not force any relationship to salt end - const handleSaltStartChange = (value: number | null) => { - if (value == null) { - setSaltStart(0) - return - } - - setSaltStart(Math.max(0, value)) - } - - // Keep salt end non-negative, but do not force any relationship to salt start - const handleSaltEndChange = (value: number | null) => { - if (value == null) { - setSaltEnd(0) - return - } - - setSaltEnd(Math.max(0, value)) - } - - // Keep salt alpha non-negative - const handleSaltAlphaChange = (value: number | null) => { - if (value == null) { - setSaltAlpha(0) - return - } - - setSaltAlpha(Math.max(0, value)) - } - // Keep fraction count at least 1 const handleFractionCountChange = (value: number | null) => { if (value == null) { @@ -315,7 +285,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 78f5c5e1..b3acd3d9 100644 --- a/frontend/jbio-app/src/pages/IonExchangeFractionation.tsx +++ b/frontend/jbio-app/src/pages/IonExchangeFractionation.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useRef, useState } from 'react' +import React, { useMemo, useState } from 'react' import { Alert, Box, @@ -206,7 +206,7 @@ const IonExchangeFractionation: React.FC = () => { setUseLogScale(checked)} />} label='Log Scale' sx={{ marginBottom: '0.25rem' }} /> setShowWash(checked)} />} label='Show Wash' sx={{ marginBottom: '0.25rem' }} /> - +