1- import React , { FunctionComponent , useEffect , useState } from "react" ;
1+ import React , { FunctionComponent , useEffect , useState , useMemo } from "react" ;
22import { IAnalysisDataProps } from "./analysis-data.interfaces" ;
33import { socket } from "../../../app.component" ;
44import { Chart } from "react-google-charts" ;
55import axios from "axios" ;
6+ import { Dropdown , Modal , Button } from "react-bootstrap" ;
67import './analysis-data.component.css' ;
78
9+ const POLLING_INTERVAL_MS = 10000 ;
10+
811const AnalysisDataComponent : FunctionComponent < IAnalysisDataProps > = ( { data } ) => {
912 const [ analysisData , setAnalysisData ] = useState ( data ) ;
1013 const [ coverageData , setCoverageData ] = useState < any [ ] > ( [ ] ) ;
14+ const [ coverageMap , setCoverageMap ] = useState ( new Map < string , any > ( ) ) ;
1115 const [ listenerRunning , setListenerRunning ] = useState ( false ) ;
12- const [ error , setError ] = useState ( "" ) ;
13- const [ metric , setMetric ] = useState < 'fold_coverage' > ( 'fold_coverage' ) ;
16+ const [ error , setError ] = useState < string | null > ( null ) ;
17+ const [ fetchError , setFetchError ] = useState < string | null > ( null ) ;
18+ const [ metric , setMetric ] = useState < 'depth' | 'breadth' > ( 'depth' ) ;
1419 const [ showConfirmModal , setShowConfirmModal ] = useState ( false ) ;
20+ type TimeUnit = 'seconds' | 'minutes' | 'hours' | 'days' ;
21+ const [ timeUnit , setTimeUnit ] = useState < TimeUnit > ( 'seconds' ) ;
1522
16- // Extract threshold from the first query, default to 100 if invalid
1723 const threshold = data . data . queries [ 0 ] ?. threshold ? parseFloat ( data . data . queries [ 0 ] . threshold ) : 100 ;
1824
25+ const unitLabels : Record < TimeUnit , string > = {
26+ seconds : 's' ,
27+ minutes : 'min' ,
28+ hours : 'h' ,
29+ days : 'd'
30+ } ;
31+
1932 useEffect ( ( ) => {
2033 socket . emit ( 'check_fastq_file_listener' , { projectId : analysisData . data . projectId } ) ;
2134
@@ -25,13 +38,13 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
2538 const handleListenerStarted = ( data : { projectId : string } ) => {
2639 if ( data . projectId === analysisData . data . projectId ) {
2740 setListenerRunning ( true ) ;
28- setError ( "" ) ;
41+ setError ( null ) ;
2942 }
3043 } ;
3144 const handleListenerStopped = ( data : { projectId : string } ) => {
3245 if ( data . projectId === analysisData . data . projectId ) {
3346 setListenerRunning ( false ) ;
34- setError ( "" ) ;
47+ setError ( null ) ;
3548 }
3649 } ;
3750 const handleListenerError = ( data : { projectId : string ; error : string } ) => {
@@ -47,21 +60,30 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
4760 socket . on ( 'fastq_file_listener_error' , handleListenerError ) ;
4861
4962 const fetchData = async ( ) => {
63+ setFetchError ( null ) ;
5064 try {
5165 const coverageRes = await axios . get ( `http://localhost:5007/get_coverage?projectId=${ analysisData . data . projectId } ` ) ;
52- setCoverageData ( coverageRes . data ) ;
66+ const newCoverageData = coverageRes . data ;
67+ setCoverageData ( newCoverageData ) ;
68+ const map = new Map < string , any > ( ) ;
69+ newCoverageData . forEach ( ( entry : { timestamp : any ; reference : any } ) => {
70+ const key = `${ entry . timestamp } -${ entry . reference } ` ;
71+ map . set ( key , entry ) ;
72+ } ) ;
73+ setCoverageMap ( map ) ;
5374
5475 const analysisRes = await axios . get ( `http://localhost:5007/get_analysis_info?uid=${ analysisData . data . projectId } ` ) ;
5576 if ( analysisRes . data . status === 200 ) {
5677 setAnalysisData ( analysisRes . data ) ;
5778 }
5879 } catch ( err ) {
5980 console . error ( "Error fetching data:" , err ) ;
81+ setFetchError ( "Failed to fetch data. Please try again later." ) ;
6082 }
6183 } ;
6284
6385 fetchData ( ) ;
64- const interval = setInterval ( fetchData , 10000 ) ;
86+ const interval = setInterval ( fetchData , POLLING_INTERVAL_MS ) ;
6587
6688 return ( ) => {
6789 clearInterval ( interval ) ;
@@ -75,30 +97,52 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
7597 const formatCoverageData = ( ) => {
7698 const refs = [ ...new Set ( coverageData . map ( d => d . reference ) ) ] ;
7799 const times = [ ...new Set ( coverageData . map ( d => d . timestamp ) ) ] . sort ( ) ;
100+ if ( times . length === 0 ) return [ ] ;
101+
78102 const startTime = new Date ( times [ 0 ] ) . getTime ( ) ;
103+ const conversionFactors : Record < TimeUnit , number > = {
104+ seconds : 1 ,
105+ minutes : 60 ,
106+ hours : 3600 ,
107+ days : 86400
108+ } ;
109+ const factor = conversionFactors [ timeUnit ] || 1 ;
79110
80- const header = [ { type : 'number' , label : ' Elapsed Time (s)' , role : '' } ] ;
111+ const header = [ { type : 'number' , label : ` Elapsed Time (${ unitLabels [ timeUnit ] } )` , role : '' } ] ;
81112 refs . forEach ( ref => {
82113 header . push ( { type : 'number' , label : ref , role : '' } ) ;
83114 header . push ( { type : 'string' , label : 'for' , role : 'tooltip' } ) ;
84115 } ) ;
116+ if ( metric === "depth" && ! isNaN ( threshold ) ) {
117+ header . push ( { type : 'number' , label : 'Threshold' , role : '' } ) ;
118+ header . push ( { type : 'string' , label : 'for' , role : 'tooltip' } ) ;
119+ }
85120
86121 const rows = times . map ( time => {
87122 const elapsedSeconds = ( new Date ( time ) . getTime ( ) - startTime ) / 1000 ;
88- const row : ( number | string ) [ ] = [ elapsedSeconds ] ;
123+ const elapsedTime = elapsedSeconds / factor ;
124+ const row : ( number | string ) [ ] = [ elapsedTime ] ;
89125 refs . forEach ( ref => {
90- const entry = coverageData . find ( d => d . timestamp === time && d . reference === ref ) ;
91- const y = entry ? entry . fold_coverage : 0 ;
92- const tooltip = `Time: ${ time } \n${ ref } : ${ y . toFixed ( 2 ) } x` ;
126+ const key = `${ time } -${ ref } ` ;
127+ const entry = coverageMap . get ( key ) ;
128+ const y = entry ? ( metric === "depth" ? entry . depth : entry . breadth ) : 0 ;
129+ const unit = metric === "depth" ? 'X' : '%' ;
130+ const tooltip = `Time: ${ time } \nElapsed:${ elapsedTime . toFixed ( 2 ) } ${ unitLabels [ timeUnit ] } \n${ ref } : ${ y . toFixed ( 2 ) } ${ unit } ` ;
93131 row . push ( y ) ;
94132 row . push ( tooltip ) ;
95133 } ) ;
134+ if ( metric === "depth" && ! isNaN ( threshold ) ) {
135+ row . push ( threshold ) ;
136+ row . push ( `Threshold: ${ threshold } ` ) ;
137+ }
96138 return row ;
97139 } ) ;
98140
99141 return [ header , ...rows ] ;
100142 } ;
101143
144+ const formattedData = useMemo ( ( ) => formatCoverageData ( ) , [ coverageMap , metric , timeUnit ] ) ;
145+
102146 const getSankeyData = ( ) => {
103147 if ( coverageData . length === 0 ) return [ [ 'From' , 'To' , 'Weight' ] ] ;
104148 const timestamps = coverageData . map ( d => new Date ( d . timestamp ) . getTime ( ) ) ;
@@ -137,20 +181,19 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
137181 setShowConfirmModal ( false ) ;
138182 } ;
139183
140- // Compute number of references for series indexing
141184 const refs = [ ...new Set ( coverageData . map ( d => d . reference ) ) ] ;
142185 const numRefs = refs . length ;
143186
144187 const chartOptions = {
145- title : 'Fold Coverage Over Time' ,
146- hAxis : { title : ' Elapsed Time (s)' } ,
147- vAxis : { title : 'Fold Coverage (x )', minValue : 0 } ,
188+ title : ` ${ metric === "depth" ? "Depth of Coverage" : "Breadth of Coverage" } Over Time` ,
189+ hAxis : { title : ` Elapsed Time (${ unitLabels [ timeUnit ] } )` } ,
190+ vAxis : { title : metric === "depth" ? 'Depth (X)' : 'Breadth (% )', minValue : 0 } ,
148191 legend : { position : 'bottom' } ,
149192 colors : [ '#00B0BD' , '#004E5A' , '#FF6A45' , '#27AE60' ] ,
150193 chartArea : { width : '80%' , height : '70%' } ,
151194 animation : { startup : true , duration : 1000 , easing : 'out' } ,
152- series : ! isNaN ( threshold ) ? {
153- [ numRefs ] : { lineDashStyle : [ 4 , 4 ] , color : 'red' , lineWidth : 2 }
195+ series : metric === "depth" && ! isNaN ( threshold ) ? {
196+ [ refs . length ] : { lineDashStyle : [ 4 , 4 ] , color : 'red' , lineWidth : 2 , pointSize : 0 }
154197 } : { }
155198 } ;
156199
@@ -183,6 +226,7 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
183226 </ div >
184227 </ div >
185228 { error && < div className = "ont-alert nano-alert-danger" > { error } </ div > }
229+ { fetchError && < div className = "ont-alert nano-alert-danger pl-2" > { fetchError } </ div > }
186230 < div className = "nano-actions" >
187231 { listenerRunning ? (
188232 < button className = "btn btn-danger nano-btn" onClick = { handleStopFileListener } >
@@ -202,22 +246,37 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
202246
203247 { /* Coverage Plot */ }
204248 < div className = "ont-card nano-chart-card" >
205- < div className = "nano-card-header" >
249+ < div className = "nano-card-header d-flex justify-content-between align-items-center " >
206250 < h3 className = "nano-card-title" > Coverage Over Time</ h3 >
207- < select
208- value = { metric }
209- onChange = { ( e ) => setMetric ( e . target . value as 'fold_coverage' ) }
210- className = "form-control w-auto d-inline-block ml-2"
211- >
212- < option value = "fold_coverage" > Average Depth</ option >
213- < option value = "breadth" > Breadth of Coverage (%)</ option >
214- </ select >
251+ < div className = "d-flex gap-2" >
252+ < Dropdown >
253+ < Dropdown . Toggle variant = "secondary" id = "metricDropdown" size = "sm" >
254+ { metric . charAt ( 0 ) . toUpperCase ( ) + metric . slice ( 1 ) }
255+ </ Dropdown . Toggle >
256+ < Dropdown . Menu >
257+ < Dropdown . Item onClick = { ( ) => setMetric ( 'depth' ) } > Depth</ Dropdown . Item >
258+ < Dropdown . Item onClick = { ( ) => setMetric ( 'breadth' ) } > Breadth</ Dropdown . Item >
259+ </ Dropdown . Menu >
260+ </ Dropdown >
261+ < Dropdown >
262+ < Dropdown . Toggle variant = "secondary" id = "timeUnitDropdown" size = "sm" >
263+ { timeUnit . charAt ( 0 ) . toUpperCase ( ) + timeUnit . slice ( 1 ) }
264+ </ Dropdown . Toggle >
265+ < Dropdown . Menu >
266+ < Dropdown . Item onClick = { ( ) => setTimeUnit ( 'seconds' ) } > Seconds</ Dropdown . Item >
267+ < Dropdown . Item onClick = { ( ) => setTimeUnit ( 'minutes' ) } > Minutes</ Dropdown . Item >
268+ < Dropdown . Item onClick = { ( ) => setTimeUnit ( 'hours' ) } > Hours</ Dropdown . Item >
269+ < Dropdown . Item onClick = { ( ) => setTimeUnit ( 'days' ) } > Days</ Dropdown . Item >
270+ </ Dropdown . Menu >
271+ </ Dropdown >
272+ </ div >
215273 </ div >
216274 < div className = "nano-card-body" >
217- { coverageData . length > 0 ? (
275+ { timeUnit && coverageData . length > 0 ? (
218276 < Chart
277+ key = { metric }
219278 chartType = "LineChart"
220- data = { formatCoverageData ( ) }
279+ data = { formattedData }
221280 options = { chartOptions }
222281 width = "100%"
223282 height = "400px"
@@ -267,26 +326,22 @@ const AnalysisDataComponent: FunctionComponent<IAnalysisDataProps> = ({ data })
267326 </ div >
268327
269328 { /* Confirmation Modal */ }
270- { showConfirmModal && (
271- < div className = "nano-modal-overlay" >
272- < div className = "nano-modal" >
273- < div className = "nano-modal-header" >
274- < h4 className = "nano-modal-title" > Confirm Removal</ h4 >
275- </ div >
276- < div className = "nano-modal-body" >
277- < p > Are you sure you want to remove this analysis? This action cannot be undone.</ p >
278- </ div >
279- < div className = "nano-modal-footer" >
280- < button className = "btn btn-outline-secondary nano-btn" onClick = { cancelRemoveAnalysis } >
281- Cancel
282- </ button >
283- < button className = "btn btn-danger nano-btn" onClick = { confirmRemoveAnalysis } >
284- Remove
285- </ button >
286- </ div >
287- </ div >
288- </ div >
289- ) }
329+ < Modal show = { showConfirmModal } onHide = { cancelRemoveAnalysis } >
330+ < Modal . Header closeButton >
331+ < Modal . Title > Confirm Removal</ Modal . Title >
332+ </ Modal . Header >
333+ < Modal . Body >
334+ < p > Are you sure you want to remove this analysis? This action cannot be undone.</ p >
335+ </ Modal . Body >
336+ < Modal . Footer >
337+ < Button variant = "outline-secondary" onClick = { cancelRemoveAnalysis } >
338+ Cancel
339+ </ Button >
340+ < Button variant = "danger" onClick = { confirmRemoveAnalysis } >
341+ Remove
342+ </ Button >
343+ </ Modal . Footer >
344+ </ Modal >
290345 </ div >
291346 ) ;
292347} ;
0 commit comments