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 ;
0 commit comments