WinslowFan's picture
Functions: only show strings longer than 10 chars; remove chain composition from UI
6d84fd1
Raw
History Blame Contribute Delete
8 kB
import React, { useState } from 'react';
import type { DataRecord } from '../../../types/protein';
import { formatTemperature, formatPH } from '../../../utils/format';
import './DataTable.css';
interface DataTableProps {
data: DataRecord[];
loading: boolean;
total: number;
filtered: number;
}
type SortField = keyof DataRecord;
type SortDirection = 'asc' | 'desc';
export const DataTable: React.FC<DataTableProps> = ({ data, loading, total, filtered }) => {
const [sortField, setSortField] = useState<SortField>('rmsd');
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 50;
if (loading) {
return <div className="table-loading">Loading data...</div>;
}
if (data.length === 0) {
return <div className="table-empty">No data to display. Adjust filters to see results.</div>;
}
// Sorting
const sortedData = [...data].sort((a, b) => {
const aValue = a[sortField];
const bValue = b[sortField];
if (typeof aValue === 'number' && typeof bValue === 'number') {
return sortDirection === 'asc' ? aValue - bValue : bValue - aValue;
}
if (typeof aValue === 'string' && typeof bValue === 'string') {
return sortDirection === 'asc'
? aValue.localeCompare(bValue)
: bValue.localeCompare(aValue);
}
return 0;
});
// Pagination
const totalPages = Math.ceil(sortedData.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedData = sortedData.slice(startIndex, endIndex);
const handleSort = (field: SortField) => {
if (sortField === field) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection('asc');
}
setCurrentPage(1);
};
const getSortIcon = (field: SortField) => {
if (sortField !== field) return '↕';
return sortDirection === 'asc' ? '↑' : '↓';
};
return (
<div className="data-table-container">
<div className="table-header">
<h3 className="text-xl font-semibold text-slate-900">Alternative observations</h3>
<div className="table-stats">
<span>Showing {paginatedData.length} of {filtered.toLocaleString()} filtered</span>
<span className="separator">|</span>
<span>Total: {total.toLocaleString()}</span>
</div>
</div>
<div className="table-wrapper">
<table className="data-table">
<thead>
<tr>
<th onClick={() => handleSort('pdb_id_b')}>
PDB ID {getSortIcon('pdb_id_b')}
</th>
<th onClick={() => handleSort('auth_asym_id_b')}>
Chain ID {getSortIcon('auth_asym_id_b')}
</th>
<th onClick={() => handleSort('tm_score')}>
TM-score {getSortIcon('tm_score')}
</th>
<th onClick={() => handleSort('rmsd')}>
RMSD (Å) {getSortIcon('rmsd')}
</th>
<th>
Experimental Method
</th>
<th>
Temp (K)
</th>
<th>
pH
</th>
<th onClick={() => handleSort('binding_status')}>
Binding status {getSortIcon('binding_status')}
</th>
<th onClick={() => handleSort('delta_rosetta')}>
ΔRosetta {getSortIcon('delta_rosetta')}
</th>
<th onClick={() => handleSort('delta_foldx')}>
ΔFoldX {getSortIcon('delta_foldx')}
</th>
<th onClick={() => handleSort('delta_evoef2')}>
ΔEvoEF2 {getSortIcon('delta_evoef2')}
</th>
<th onClick={() => handleSort('delta_rw')}>
ΔRW {getSortIcon('delta_rw')}
</th>
<th onClick={() => handleSort('delta_rw_plus')}>
ΔRW+ {getSortIcon('delta_rw_plus')}
</th>
<th onClick={() => handleSort('state_id_b')}>
State ID {getSortIcon('state_id_b')}
</th>
<th onClick={() => handleSort('state_fidelity')}>
State fidelity {getSortIcon('state_fidelity')}
</th>
<th onClick={() => handleSort('avg_sim')}>
State avg. similarity {getSortIcon('avg_sim')}
</th>
<th onClick={() => handleSort('observation_fidelity')}>
Observation fidelity {getSortIcon('observation_fidelity')}
</th>
<th onClick={() => handleSort('structure_sim')}>
Observation similarity {getSortIcon('structure_sim')}
</th>
</tr>
</thead>
<tbody>
{paginatedData.map((record, index) => (
<tr key={`${record.pdb_id_a}-${record.auth_asym_id_a}-${record.pdb_id_b}-${record.auth_asym_id_b}-${index}`}>
<td>
<span className="chain-id">{record.pdb_id_b.toUpperCase()}</span>
</td>
<td>
<span className="asym-id">{record.auth_asym_id_b}</span>
</td>
<td className="numeric">{record.tm_score.toFixed(3)}</td>
<td className="numeric">{record.rmsd.toFixed(2)}</td>
<td className="experimental-method">
{record.exptl_method || <span className="placeholder"></span>}
</td>
<td className="numeric">
{formatTemperature(record.temp)}
</td>
<td className="numeric">
{formatPH(record.pH)}
</td>
<td className="capitalize">
{record.binding_status || <span className="placeholder"></span>}
</td>
{(['delta_rosetta', 'delta_foldx', 'delta_evoef2', 'delta_rw', 'delta_rw_plus'] as const).map(field => {
const val = record[field];
return (
<td key={field} className={`numeric delta-cell${val == null ? '' : val > 0 ? ' delta-pos' : val < 0 ? ' delta-neg' : ''}`}>
{val == null ? <span className="placeholder"></span> : `${val > 0 ? '+' : ''}${val.toFixed(2)}`}
</td>
);
})}
<td className="font-mono">{record.state_id_b ?? <span className="placeholder"></span>}</td>
<td>{record.state_fidelity ?? <span className="placeholder"></span>}</td>
<td className="numeric">{record.avg_sim ?? <span className="placeholder"></span>}</td>
<td>{record.observation_fidelity ?? <span className="placeholder"></span>}</td>
<td className="numeric">{record.structure_sim != null ? record.structure_sim.toFixed(3) : <span className="placeholder"></span>}</td>
</tr>
))}
</tbody>
</table>
</div>
{totalPages > 1 && (
<div className="pagination">
<button
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
>
Previous
</button>
<span className="page-info">
Page {currentPage} of {totalPages}
</span>
<button
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
>
Next
</button>
</div>
)}
</div>
);
};