Displaying Processing Results in the frontend

This commit is contained in:
GotthardG
2025-03-17 16:45:50 +01:00
parent 5a0047b6d5
commit 68f87f0d8d
5 changed files with 271 additions and 172 deletions

View File

@ -85,6 +85,7 @@ interface TreeRow {
id: string;
hierarchy: (string | number)[];
type: 'sample' | 'run';
experimentId?: number;
sample_id: number;
sample_name?: string;
puck_name?: string;
@ -209,6 +210,7 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => {
id: `run-${sample.sample_id}-${run.run_number}`,
hierarchy: [sample.sample_id, run.run_number],
type: 'run',
experimentId: run.id,
sample_id: sample.sample_id,
run_number: run.run_number,
beamline_parameters: run.beamline_parameters,
@ -307,8 +309,10 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => {
return (
<RunDetails
run={params.row}
runId={params.row.experimentId}
sample_id={params.row.sample_id}
basePath={basePath}
onHeightChange={(height: number) => handleDetailPanelHeightChange(params.row.id, height)} // Pass callback for dynamic height
onHeightChange={height => handleDetailPanelHeightChange(params.row.id, height)}
/>
);
}

View File

@ -1,196 +1,279 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Accordion,
AccordionSummary,
AccordionDetails,
Typography,
Grid,
Modal,
Box
Accordion, AccordionSummary, AccordionDetails, Typography, Grid, Modal, Box
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import './SampleImage.css';
import { DataGridPremium, GridColDef } from "@mui/x-data-grid-premium";
import { SamplesService } from "../../openapi";
interface RunDetailsProps {
run: ExperimentParameters;
run: TreeRow;
runId: number;
sample_id: number;
basePath: string;
onHeightChange?: (height: number) => void; // Callback to notify the parent about height changes
onHeightChange?: (height: number) => void;
}
const RunDetails: React.FC<RunDetailsProps> = ({ run, onHeightChange, basePath }) => {
const containerRef = useRef<HTMLDivElement | null>(null); // Ref to track component height
interface ExperimentParameters {
run_number: number;
id: number;
sample_id: number;
beamline_parameters: BeamlineParameters;
images: Image[];
}
interface ProcessingResults {
pipeline: string;
resolution: number;
unit_cell: string;
spacegroup: string;
rmerge: number;
rmeas: number;
isig: number;
cc: number;
cchalf: number;
completeness: number;
multiplicity: number;
nobs: number;
total_refl: number;
unique_refl: number;
comments?: string | null;
}
const RunDetails: React.FC<RunDetailsProps> = ({ run, onHeightChange, basePath, runId, sample_id }) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const [currentHeight, setCurrentHeight] = useState<number>(0);
const [modalOpen, setModalOpen] = useState<boolean>(false); // For modal state
const [selectedImage, setSelectedImage] = useState<string | null>(null); // Tracks the selected image for the modal
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [expandedResults, setExpandedResults] = useState(false);
const [processingResult, setProcessingResult] = useState<ProcessingResults[] | null>(null);
const { beamline_parameters, images } = run;
const { synchrotron, beamline, detector } = beamline_parameters;
const {beamline_parameters, images} = run;
const {synchrotron, beamline, detector} = beamline_parameters;
useEffect(() => {
fetchResults(sample_id, runId); // fetching based on experimentId
}, [runId]);
const fetchResults = async (sample_id: number, runId: number) => {
try {
const results = await SamplesService.getResultsForRunAndSampleSamplesProcessingResultsSampleIdRunIdGet(sample_id, runId);
// Explicitly handle nested results
const mappedResults: ProcessingResults[] = results.map((res): ProcessingResults => ({
pipeline: res.result?.pipeline || 'N/A',
resolution: res.result.resolution ?? 0,
unit_cell: res.result?.unit_cell || 'N/A',
spacegroup: res.result?.spacegroup || 'N/A',
rmerge: res.result?.rmerge ?? 0,
rmeas: res.result?.rmeas ?? 0,
isig: res.result?.isig ?? 0,
cc: res.result?.cc ?? 0,
cchalf: res.result?.cchalf ?? 0,
completeness: res.result?.completeness ?? 0,
multiplicity: res.result?.multiplicity ?? 0,
nobs: res.result?.nobs ?? 0,
total_refl: res.result?.total_refl ?? 0,
unique_refl: res.result?.unique_refl ?? 0,
comments: res.result?.comments || null,
}));
setProcessingResult(mappedResults);
} catch (error) {
console.error('Error fetching results:', error);
}
};
const resultColumns: GridColDef[] = [
{field: 'pipeline', headerName: 'Pipeline', flex: 1},
{field: 'resolution', headerName: 'Resolution (Å)', flex: 1},
{field: 'unit_cell', headerName: 'Unit Cell (Å)', flex: 1.5},
{field: 'spacegroup', headerName: 'Spacegroup', flex: 1},
{field: 'rmerge', headerName: 'Rmerge', flex: 1},
{field: 'rmeas', headerName: 'Rmeas', flex: 1},
{field: 'isig', headerName: 'I/sig(I)', flex: 1},
{field: 'cc', headerName: 'CC', flex: 1},
{field: 'cchalf', headerName: 'CC(1/2)', flex: 1},
{field: 'completeness', headerName: 'Completeness (%)', flex: 1},
{field: 'multiplicity', headerName: 'Multiplicity', flex: 1},
{field: 'nobs', headerName: 'N obs.', flex: 1},
{field: 'total_refl', headerName: 'Total Reflections', flex: 1},
{field: 'unique_refl', headerName: 'Unique Reflections', flex: 1},
{field: 'comments', headerName: 'Comments', flex: 2},
];
// Calculate and notify the parent about height changes
const updateHeight = () => {
if (containerRef.current) {
const newHeight = containerRef.current.offsetHeight;
if (newHeight !== currentHeight) {
if (newHeight !== currentHeight && onHeightChange) {
setCurrentHeight(newHeight);
if (onHeightChange) {
onHeightChange(newHeight);
}
onHeightChange(newHeight);
}
}
};
useEffect(() => {
updateHeight(); // Update height on initial render
}, []);
useEffect(() => {
// Update height whenever the component content changes
const observer = new ResizeObserver(updateHeight);
if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => {
observer.disconnect();
};
}, [containerRef]);
if (containerRef.current) observer.observe(containerRef.current);
return () => observer.disconnect();
}, [containerRef.current, processingResult]);
const handleImageClick = (imagePath: string) => {
setSelectedImage(imagePath);
setModalOpen(true); // Open the modal when the image is clicked
setModalOpen(true);
};
const closeModal = () => {
setSelectedImage(null); // Clear the current image
setSelectedImage(null);
setModalOpen(false);
};
return (
<div
className="details-panel" // Add the class here
ref={containerRef} // Attach the ref to the main container
className="details-panel"
ref={containerRef}
style={{
display: 'flex',
flexDirection: 'column', // Stack children vertically
gap: '16px',
padding: '16px',
border: '1px solid #ccc',
borderRadius: '4px',
alignItems: 'flex-start',
}}
>
{/* Main Details Section */}
<div style={{ flexGrow: 1 }}>
<Typography variant="h6" gutterBottom>
Run {run.run_number} Details
</Typography>
<Typography variant="subtitle1" gutterBottom>
Beamline: {beamline} | Synchrotron: {synchrotron}
</Typography>
{/* Wrap details and images together */}
<div style={{display: 'flex', gap: '16px', alignItems: 'flex-start'}}>
{/* Main Details Section */}
<div style={{flexGrow: 1}}>
<Typography variant="h6" gutterBottom>
Run {run.run_number} Details
</Typography>
<Typography variant="subtitle1" gutterBottom>
Beamline: {beamline} | Synchrotron: {synchrotron}
</Typography>
{/* Detector Details Accordion */}
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="detector-content"
id="detector-header"
>
<Typography>
<strong>Detector Details</strong>
</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>Manufacturer: {detector?.manufacturer || 'N/A'}</Typography>
<Typography>Model: {detector?.model || 'N/A'}</Typography>
<Typography>Type: {detector?.type || 'N/A'}</Typography>
<Typography>
Beam Center (px): x: {detector?.beamCenterX_px || 'N/A'}, y: {detector?.beamCenterY_px || 'N/A'}
</Typography>
</AccordionDetails>
</Accordion>
{/* Detector Details Accordion */}
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon/>}
aria-controls="detector-content"
id="detector-header"
>
<Typography><strong>Detector Details</strong></Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>Manufacturer: {detector?.manufacturer || 'N/A'}</Typography>
<Typography>Model: {detector?.model || 'N/A'}</Typography>
<Typography>Type: {detector?.type || 'N/A'}</Typography>
<Typography>
Beam Center (px): x: {detector?.beamCenterX_px || 'N/A'},
y: {detector?.beamCenterY_px || 'N/A'}
</Typography>
</AccordionDetails>
</Accordion>
{/* Beamline Details Accordion */}
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="beamline-content"
id="beamline-header"
>
<Typography>
<strong>Beamline Details</strong>
</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>Synchrotron: {beamline_parameters?.synchrotron || 'N/A'}</Typography>
<Typography>Ring mode: {beamline_parameters?.ringMode || 'N/A'}</Typography>
<Typography>Ring current: {beamline_parameters?.ringCurrent_A || 'N/A'}</Typography>
<Typography>Beamline: {beamline_parameters?.beamline || 'N/A'}</Typography>
<Typography>Undulator: {beamline_parameters?.undulator || 'N/A'}</Typography>
<Typography>Undulator gap: {beamline_parameters?.undulatorgap_mm || 'N/A'}</Typography>
<Typography>Focusing optic: {beamline_parameters?.focusingOptic || 'N/A'}</Typography>
<Typography>Monochromator: {beamline_parameters?.monochromator || 'N/A'}</Typography>
</AccordionDetails>
</Accordion>
{/* Beamline Details Accordion */}
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon/>}>
<Typography><strong>Beamline Details</strong></Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>Synchrotron: {beamline_parameters?.synchrotron || 'N/A'}</Typography>
<Typography>Ring mode: {beamline_parameters?.ringMode || 'N/A'}</Typography>
<Typography>Ring current: {beamline_parameters?.ringCurrent_A || 'N/A'}</Typography>
<Typography>Beamline: {beamline_parameters?.beamline || 'N/A'}</Typography>
<Typography>Undulator: {beamline_parameters?.undulator || 'N/A'}</Typography>
<Typography>Undulator gap: {beamline_parameters?.undulatorgap_mm || 'N/A'}</Typography>
<Typography>Focusing optic: {beamline_parameters?.focusingOptic || 'N/A'}</Typography>
<Typography>Monochromator: {beamline_parameters?.monochromator || 'N/A'}</Typography>
</AccordionDetails>
</Accordion>
{/* Beam Characteristics Accordion */}
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="beam-content"
id="beam-header"
>
<Typography>
<strong>Beam Characteristics</strong>
{/* Beam Characteristics Accordion */}
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon/>}>
<Typography><strong>Beam Characteristics</strong></Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>Wavelength: {beamline_parameters?.wavelength || 'N/A'}</Typography>
<Typography>Energy: {beamline_parameters?.energy || 'N/A'}</Typography>
<Typography>Transmission: {beamline_parameters?.transmission || 'N/A'}</Typography>
<Typography>
Beam focus (µm): vertical: {beamline_parameters?.beamSizeHeight || 'N/A'},
horizontal:{' '}
{beamline_parameters?.beamSizeWidth || 'N/A'}
</Typography>
<Typography>Flux at sample
(ph/s): {beamline_parameters?.beamlineFluxAtSample_ph_s || 'N/A'}</Typography>
</AccordionDetails>
</Accordion>
</div>
{/* Image Section */}
<div style={{width: '900px'}}>
<Typography variant="h6" gutterBottom>
Associated Images
</Typography>
{images && images.length > 0 ? (
<Grid container spacing={1}>
{images.map((img) => (
<Grid item xs={4} key={img.id}>
<div
className="image-container"
onClick={() => handleImageClick(`${basePath || ''}${img.filepath}`)}
style={{cursor: 'pointer'}}
>
<img
src={`${basePath || ''}${img.filepath}`}
alt={img.comment || 'Image'}
className="zoom-image"
style={{
width: '100%',
maxWidth: '100%',
borderRadius: '4px',
}}
/>
</div>
</Grid>
))}
</Grid>
) : (
<Typography variant="body2" color="textSecondary">
No images available.
</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>Wavelength: {beamline_parameters?.wavelength || 'N/A'}</Typography>
<Typography>Energy: {beamline_parameters?.energy || 'N/A'}</Typography>
<Typography>Transmission: {beamline_parameters?.transmission || 'N/A'}</Typography>
<Typography>
Beam focus (µm): vertical: {beamline_parameters?.beamSizeHeight || 'N/A'}, horizontal:{' '}
{beamline_parameters?.beamSizeWidth || 'N/A'}
</Typography>
<Typography>Flux at sample (ph/s): {beamline_parameters?.beamlineFluxAtSample_ph_s || 'N/A'}</Typography>
</AccordionDetails>
</Accordion>
)}
</div>
</div>
{/* Image Section */}
<div style={{ width: '900px' }}>
<Typography variant="h6" gutterBottom>
Associated Images
</Typography>
{images && images.length > 0 ? (
<Grid container spacing={1}>
{images.map((img) => (
<Grid item xs={4} key={img.id}>
<div
className="image-container"
onClick={() => handleImageClick(`${basePath || ''}${img.filepath}`)} // Open modal with image
style={{
cursor: 'pointer',
}}
>
<img
src={`${basePath || ''}${img.filepath}`} // Ensure basePath
alt={img.comment || 'Image'}
className="zoom-image"
style={{
width: '100%', // Ensure the image takes the full width of its container
maxWidth: '100%', // Prevent any overflow
borderRadius: '4px',
}}
/>
</div>
</Grid>
))}
</Grid>
) : (
<Typography variant="body2" color="textSecondary">
No images available.
</Typography>
)}
{/* Processing Results Accordion - Full Width Below */}
<div style={{width: '100%'}}>
<Accordion expanded={expandedResults} onChange={(e, expanded) => setExpandedResults(expanded)}>
<AccordionSummary expandIcon={<ExpandMoreIcon/>}>
<Typography><strong>Processing Results</strong></Typography>
</AccordionSummary>
<AccordionDetails style={{width: '100%', overflowX: 'auto'}}>
{processingResult ? (
<div style={{width: '100%'}}>
<DataGridPremium
rows={processingResult.map((res, idx) => ({id: idx, ...res}))}
columns={resultColumns}
autoHeight
hideFooter
columnVisibilityModel={{id: false}}
disableColumnResize={false}
/>
</div>
) : (
<Typography variant="body2" color="textSecondary">Loading results...</Typography>
)}
</AccordionDetails>
</Accordion>
</div>
{/* Modal for Zoomed Image */}
@ -224,5 +307,4 @@ const RunDetails: React.FC<RunDetailsProps> = ({ run, onHeightChange, basePath }
</div>
);
};
export default RunDetails;