Skip to content

Commit 36077f3

Browse files
committed
add sequence coverage visualization plot with alignment data
1 parent 407d92e commit 36077f3

3 files changed

Lines changed: 270 additions & 14 deletions

File tree

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import React, { useRef, useLayoutEffect, useState } from 'react';
2+
3+
interface Alignment {
4+
start: number;
5+
end: number;
6+
strand: string;
7+
}
8+
9+
interface AlignmentViewerProps {
10+
refLength: number;
11+
alignments: Alignment[];
12+
}
13+
14+
const AlignmentViewer: React.FC<AlignmentViewerProps> = ({ refLength, alignments }) => {
15+
const containerRef = useRef<HTMLDivElement>(null);
16+
const [svgWidth, setSvgWidth] = useState(800);
17+
18+
useLayoutEffect(() => {
19+
if (containerRef.current) {
20+
setSvgWidth(containerRef.current.offsetWidth);
21+
}
22+
const handleResize = () => {
23+
if (containerRef.current) {
24+
setSvgWidth(containerRef.current.offsetWidth);
25+
}
26+
};
27+
window.addEventListener('resize', handleResize);
28+
return () => window.removeEventListener('resize', handleResize);
29+
}, []);
30+
31+
// Layout constants for a larger, clearer design
32+
const topMargin = 30; // Increased to place "Query Sequence" label above the bar
33+
const bottomMargin = 30; // Reduced to make the visualization shorter
34+
const leftMargin = 60; // Increased to provide space for "Aligned Reads" label
35+
const rightMargin = 40; // Added to ensure scale labels are fully visible
36+
const queryBarHeight = 25;
37+
const readHeight = 15;
38+
const rowGap = 8; // Reduced to make the visualization shorter
39+
const noReadsPlaceholderHeight = 25; // Height for "No aligned reads" message
40+
41+
// Stacking algorithm to place reads in rows without overlap
42+
const rows: Alignment[][] = [];
43+
alignments.sort((a, b) => a.start - b.start);
44+
alignments.forEach(alignment => {
45+
let placed = false;
46+
for (const row of rows) {
47+
const lastAlignment = row[row.length - 1];
48+
if (lastAlignment.end < alignment.start) {
49+
row.push(alignment);
50+
placed = true;
51+
break;
52+
}
53+
}
54+
if (!placed) {
55+
rows.push([alignment]);
56+
}
57+
});
58+
59+
// Calculate heights
60+
const readAreaHeight = rows.length > 0
61+
? rows.length * readHeight + (rows.length - 1) * rowGap
62+
: noReadsPlaceholderHeight;
63+
const contentHeight = queryBarHeight + (readAreaHeight > 0 ? rowGap + readAreaHeight : 0);
64+
const xAxisYPosition = topMargin + contentHeight + rowGap; // Position x-axis below content
65+
const svgHeight = xAxisYPosition + bottomMargin;
66+
67+
const sequenceXStart = leftMargin;
68+
const sequenceWidth = svgWidth - leftMargin - rightMargin;
69+
const scale = sequenceWidth / refLength;
70+
71+
// X-axis tick positions
72+
const tickPositions = [0, Math.floor(refLength / 4), Math.floor(refLength / 2), Math.floor(3 * refLength / 4), refLength];
73+
74+
return (
75+
<div ref={containerRef} style={{ width: '100%' }}>
76+
<svg width="100%" height={svgHeight} viewBox={`0 0 ${svgWidth} ${svgHeight}`}>
77+
{/* Background */}
78+
<rect x={0} y={0} width={svgWidth} height={svgHeight} fill="#FFF" />
79+
80+
{/* Query bar */}
81+
<rect x={sequenceXStart} y={topMargin} width={sequenceWidth} height={queryBarHeight} fill="#ccc" stroke="#000" strokeWidth={2} />
82+
{/* Arrowheads for query sequence */}
83+
<polygon points={`${sequenceXStart + sequenceWidth - 10},${topMargin + 5} ${sequenceXStart + sequenceWidth},${topMargin + queryBarHeight / 2} ${sequenceXStart + sequenceWidth - 10},${topMargin + queryBarHeight - 5}`} fill="#000" />
84+
<polygon points={`${sequenceXStart + 10},${topMargin + 5} ${sequenceXStart},${topMargin + queryBarHeight / 2} ${sequenceXStart + 10},${topMargin + queryBarHeight - 5}`} fill="#000" />
85+
86+
{/* Reads or "No aligned reads" message */}
87+
{rows.length === 0 ? (
88+
<g>
89+
<rect
90+
x={sequenceXStart}
91+
y={topMargin + queryBarHeight + rowGap}
92+
width={sequenceWidth}
93+
height={noReadsPlaceholderHeight}
94+
fill="#f5f5f5"
95+
stroke="#bbb"
96+
strokeDasharray="4 2"
97+
/>
98+
<text
99+
x={sequenceXStart + sequenceWidth / 2}
100+
y={topMargin + queryBarHeight + rowGap + noReadsPlaceholderHeight / 2}
101+
textAnchor="middle"
102+
dominantBaseline="middle"
103+
fontSize={15}
104+
fill="#888"
105+
fontStyle="italic"
106+
>
107+
No aligned reads
108+
</text>
109+
</g>
110+
) : (
111+
rows.map((row, rowIndex) =>
112+
row.map((alignment, alignIndex) => {
113+
const x = sequenceXStart + alignment.start * scale;
114+
const width = (alignment.end - alignment.start) * scale;
115+
const y = topMargin + queryBarHeight + rowGap + rowIndex * (readHeight + rowGap);
116+
return (
117+
<g key={`${rowIndex}-${alignIndex}`}>
118+
<rect
119+
x={x}
120+
y={y}
121+
width={width}
122+
height={readHeight}
123+
fill={alignment.strand === '+' ? '#00B0BD' : '#FF6A45'}
124+
stroke="#000"
125+
strokeWidth={1}
126+
/>
127+
{/* Arrowheads for strand direction */}
128+
{alignment.strand === '+' ? (
129+
<polygon
130+
points={`${x + width - 5},${y + 2} ${x + width},${y + readHeight / 2} ${x + width - 5},${y + readHeight - 2}`}
131+
fill="#000"
132+
/>
133+
) : (
134+
<polygon
135+
points={`${x + 5},${y + 2} ${x},${y + readHeight / 2} ${x + 5},${y + readHeight - 2}`}
136+
fill="#000"
137+
/>
138+
)}
139+
</g>
140+
);
141+
})
142+
)
143+
)}
144+
145+
{/* X-axis */}
146+
<line x1={sequenceXStart} y1={xAxisYPosition} x2={sequenceXStart + sequenceWidth} y2={xAxisYPosition} stroke="#000" strokeWidth={1} />
147+
{tickPositions.map((pos, index) => {
148+
const x = sequenceXStart + (pos * scale);
149+
return (
150+
<g key={index}>
151+
<line x1={x} y1={xAxisYPosition} x2={x} y2={xAxisYPosition + 5} stroke="#000" strokeWidth={1} />
152+
<text x={x} y={xAxisYPosition + 15} textAnchor="middle" fontSize={10}>
153+
{pos.toLocaleString()}
154+
</text>
155+
</g>
156+
);
157+
})}
158+
159+
{/* Y-axis labels */}
160+
<text
161+
x={svgWidth / 2}
162+
y={topMargin - 10} // Positioned above the query bar
163+
fontSize={12}
164+
dominantBaseline="middle"
165+
textAnchor="middle"
166+
fontWeight="bold"
167+
style={{ textTransform: 'uppercase' }}
168+
>
169+
QUERY SEQUENCE
170+
</text>
171+
{rows.length > 0 && (
172+
<text
173+
x={leftMargin - 25} // Move further left to accommodate rotation
174+
y={topMargin + queryBarHeight + rowGap + readAreaHeight / 2}
175+
fontSize={12}
176+
dominantBaseline="middle"
177+
textAnchor="middle"
178+
transform={`rotate(-90, ${leftMargin - 25}, ${topMargin + queryBarHeight + rowGap + readAreaHeight / 2})`}
179+
>
180+
Aligned Reads
181+
</text>
182+
)}
183+
</svg>
184+
</div>
185+
);
186+
};
187+
188+
export default AlignmentViewer;

frontend/src/modules/analysis/analysis-data/analysis-data.component.tsx

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { socket } from "../../../app.component";
44
import { Chart } from "react-google-charts";
55
import axios from "axios";
66
import { Dropdown, Modal, Button, OverlayTrigger, Tooltip } from "react-bootstrap";
7+
import AlignmentViewer from "./alignment-viewer.component";
78
import './analysis-data.component.css';
89

910
const API_ENDPOINT = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:5007';
@@ -16,9 +17,11 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
1617
const [coverageMap, setCoverageMap] = useState(new Map<string, any>());
1718
const [listenerRunning, setListenerRunning] = useState(false);
1819
const [error, setError] = useState<string | null>(null);
19-
const [fetchError, setFetchError] = useState<string | null>(null); // Still available but not used for this case
20+
const [fetchError, setFetchError] = useState<string | null>(null);
2021
const [metric, setMetric] = useState<'depth' | 'breadth'>('depth');
2122
const [showConfirmModal, setShowConfirmModal] = useState(false);
23+
const [selectedReference, setSelectedReference] = useState<string | null>(null);
24+
const [alignmentData, setAlignmentData] = useState<{ref_length: number, alignments: any[]} | null>(null);
2225
type TimeUnit = 'seconds' | 'minutes' | 'hours' | 'days';
2326
const [timeUnit, setTimeUnit] = useState<TimeUnit>('seconds');
2427
const [isDatabaseReady, setIsDatabaseReady] = useState(false);
@@ -39,11 +42,10 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
3942
return res.data.is_ready;
4043
} catch (error) {
4144
console.error("Error checking database status:", error);
42-
return false; // Assume not ready if there's an error
45+
return false;
4346
}
4447
};
4548

46-
4749
useEffect(() => {
4850
socket.emit('check_fastq_file_listener', { projectId: analysisData.data.projectId });
4951

@@ -75,7 +77,7 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
7577
socket.on('fastq_file_listener_error', handleListenerError);
7678

7779
const fetchData = async () => {
78-
setError(null); // Clear previous errors before fetching
80+
setError(null);
7981
try {
8082
const coverageRes = await axios.get(`${API_ENDPOINT}/get_coverage?projectId=${analysisData.data.projectId}`);
8183
const analysisRes = await axios.get(`${API_ENDPOINT}/get_analysis_info?uid=${analysisData.data.projectId}`);
@@ -95,24 +97,19 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
9597
} catch (err) {
9698
console.error("Error fetching data:", err);
9799
if (err.response && err.response.data && err.response.data.error) {
98-
// Set specific error message from server
99100
setError(err.response.data.error);
100101
} else if (err.response) {
101-
// Handle other server errors with a generic message
102102
setError("An error occurred while fetching data.");
103103
} else {
104-
// Handle network errors
105104
setError("Failed to connect to the server. Please check your network connection.");
106105
}
107106
}
108107

109-
// Check database status
110108
const checkStats = await checkDatabaseStatus(analysisData.data.projectId);
111109
setIsDatabaseReady(checkStats);
112110
if (!checkStats) {
113111
setError("Database is not ready. Please wait for the database to be initialized.");
114112
}
115-
116113
};
117114

118115
fetchData();
@@ -127,6 +124,21 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
127124
};
128125
}, [analysisData.data.projectId]);
129126

127+
useEffect(() => {
128+
const fetchAlignmentData = async () => {
129+
if (selectedReference) {
130+
try {
131+
const res = await axios.get(`${API_ENDPOINT}/get_alignments?projectId=${analysisData.data.projectId}&reference=${selectedReference}`);
132+
setAlignmentData(res.data);
133+
} catch (err) {
134+
console.error("Error fetching alignment data:", err);
135+
setAlignmentData(null);
136+
}
137+
}
138+
};
139+
fetchAlignmentData();
140+
}, [selectedReference, analysisData.data.projectId]);
141+
130142
const formatCoverageData = () => {
131143
const refs = [...new Set(coverageData.map(d => d.reference))].filter(ref => ref !== 'Unmapped');
132144
const times = [...new Set(coverageData.map(d => d.timestamp))].sort();
@@ -285,6 +297,34 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
285297
</div>
286298
</div>
287299

300+
{/* Alignment Visualization */}
301+
<div className="nano-card nano-chart-card">
302+
<div className="nano-card-header d-flex justify-content-between align-items-center">
303+
<h3 className="nano-card-title">Sequence Coverage Visualization</h3>
304+
<Dropdown>
305+
<Dropdown.Toggle variant="secondary" id="referenceDropdown" size="sm">
306+
{selectedReference || "Select Reference"}
307+
</Dropdown.Toggle>
308+
<Dropdown.Menu>
309+
{analysisData.data.queries.map((query, index) => (
310+
<Dropdown.Item key={index} onClick={() => setSelectedReference(query.name)}>
311+
{query.name}
312+
</Dropdown.Item>
313+
))}
314+
</Dropdown.Menu>
315+
</Dropdown>
316+
</div>
317+
<div className="nano-card-body">
318+
{selectedReference && alignmentData ? (
319+
<AlignmentViewer refLength={alignmentData.ref_length} alignments={alignmentData.alignments} />
320+
) : (
321+
<div className="nano-empty-state">
322+
<p>Select a reference to view sequence coverage.</p>
323+
</div>
324+
)}
325+
</div>
326+
</div>
327+
288328
{/* Coverage Plot */}
289329
<div className="nano-card nano-chart-card">
290330
<div className="nano-card-header d-flex justify-content-between align-items-center">

0 commit comments

Comments
 (0)