@@ -29,6 +29,25 @@ type DataWithZorder = Data & { zorder?: number }
2929
3030const noop = ( ) => { }
3131
32+ /** Split band data into contiguous segments at null gaps (so Plotly doesn't interpolate across gaps) */
33+ function splitBandSegments ( timestamps : string [ ] , upper : ( number | null ) [ ] , lower : ( number | null ) [ ] ) {
34+ const segments : Array < { timestamps : string [ ] ; upper : ( number | null ) [ ] ; lower : ( number | null ) [ ] } > = [ ]
35+ let cur : typeof segments [ number ] | null = null
36+ for ( let i = 0 ; i < timestamps . length ; i ++ ) {
37+ if ( upper [ i ] === null || lower [ i ] === null ) {
38+ if ( cur && cur . timestamps . length > 0 ) segments . push ( cur )
39+ cur = null
40+ } else {
41+ if ( ! cur ) cur = { timestamps : [ ] , upper : [ ] , lower : [ ] }
42+ cur . timestamps . push ( timestamps [ i ] )
43+ cur . upper . push ( upper [ i ] )
44+ cur . lower . push ( lower [ i ] )
45+ }
46+ }
47+ if ( cur && cur . timestamps . length > 0 ) segments . push ( cur )
48+ return segments
49+ }
50+
3251export type HasDeviceIdx = { deviceIdx : number }
3352export type HasMetric = { metric : 'primary' | 'secondary' }
3453
@@ -844,130 +863,78 @@ export const AwairChart = memo(function AwairChart(
844863 }
845864
846865 // Stddev fill regions (±σ shaded areas) - from smoothed data when available
847- // Primary stddev region (only for first device)
848866 // Split into segments at gaps (null values) so Plotly doesn't interpolate across gaps
849- if ( ! isRawData && deviceData . length > 0 ) {
850- const d = deviceData [ 0 ]
851- // Use smoothed data for bands when smoothing enabled, otherwise raw
852- const bandTimestamps = hasSmoothing ? d . smoothedTimestamps : d . timestamps
853- // Propagate nulls: null + anything = null (avoid JS's null + null = 0)
854- const bandUpper = hasSmoothing ? d . upperValues : d . avgValues . map ( ( avg , i ) =>
855- avg === null || d . stddevValues [ i ] === null ? null : avg + d . stddevValues [ i ]
856- )
857- const bandLower = hasSmoothing ? d . lowerValues : d . avgValues . map ( ( avg , i ) =>
858- avg === null || d . stddevValues [ i ] === null ? null : avg - d . stddevValues [ i ]
859- )
860-
861- // Split data into segments at null values
862- const segments : Array < { timestamps : string [ ] ; upper : ( number | null ) [ ] ; lower : ( number | null ) [ ] } > = [ ]
863- let currentSegment : { timestamps : string [ ] ; upper : ( number | null ) [ ] ; lower : ( number | null ) [ ] } | null = null
864-
865- for ( let i = 0 ; i < bandTimestamps . length ; i ++ ) {
866- if ( bandUpper [ i ] === null || bandLower [ i ] === null ) {
867- // Gap point - end current segment
868- if ( currentSegment && currentSegment . timestamps . length > 0 ) {
869- segments . push ( currentSegment )
870- }
871- currentSegment = null
872- } else {
873- // Real data point
874- if ( ! currentSegment ) {
875- currentSegment = { timestamps : [ ] , upper : [ ] , lower : [ ] }
876- }
877- currentSegment . timestamps . push ( bandTimestamps [ i ] )
878- currentSegment . upper . push ( bandUpper [ i ] )
879- currentSegment . lower . push ( bandLower [ i ] )
880- }
881- }
882- // Don't forget the last segment
883- if ( currentSegment && currentSegment . timestamps . length > 0 ) {
884- segments . push ( currentSegment )
885- }
867+ if ( ! isRawData ) {
868+ deviceData . forEach ( d => {
869+ // Primary stddev bands
870+ const bandTimestamps = hasSmoothing ? d . smoothedTimestamps : d . timestamps
871+ const bandUpper = hasSmoothing ? d . upperValues : d . avgValues . map ( ( avg , i ) =>
872+ avg === null || d . stddevValues [ i ] === null ? null : avg + d . stddevValues [ i ]
873+ )
874+ const bandLower = hasSmoothing ? d . lowerValues : d . avgValues . map ( ( avg , i ) =>
875+ avg === null || d . stddevValues [ i ] === null ? null : avg - d . stddevValues [ i ]
876+ )
886877
887- // Create traces for each segment
888- segments . forEach ( ( seg , segIdx ) => {
889- traces . push ( {
890- x : seg . timestamps ,
891- y : seg . lower ,
892- mode : 'lines' ,
893- line : { color : 'transparent' } ,
894- name : `${ config . label } Lower` ,
895- showlegend : false ,
896- hoverinfo : 'skip' ,
897- } )
898- traces . push ( {
899- x : seg . timestamps ,
900- y : seg . upper ,
901- fill : 'tonexty' ,
902- fillcolor : `${ d . primaryLineProps . color } ${ opacityHex } ` ,
903- line : { color : 'transparent' } ,
904- mode : 'lines' ,
905- name : segIdx === 0 ? `±σ ${ config . label } ` : `±σ ${ config . label } (cont)` ,
906- showlegend : false ,
907- hoverinfo : 'skip' ,
878+ const segments = splitBandSegments ( bandTimestamps , bandUpper , bandLower )
879+ segments . forEach ( ( seg , segIdx ) => {
880+ traces . push ( {
881+ x : seg . timestamps ,
882+ y : seg . lower ,
883+ mode : 'lines' ,
884+ line : { color : 'transparent' } ,
885+ name : `${ config . label } Lower` ,
886+ showlegend : false ,
887+ hoverinfo : 'skip' ,
888+ } )
889+ traces . push ( {
890+ x : seg . timestamps ,
891+ y : seg . upper ,
892+ fill : 'tonexty' ,
893+ fillcolor : `${ d . primaryLineProps . color } ${ opacityHex } ` ,
894+ line : { color : 'transparent' } ,
895+ mode : 'lines' ,
896+ name : segIdx === 0 ? `±σ ${ config . label } ` : `±σ ${ config . label } (cont)` ,
897+ showlegend : false ,
898+ hoverinfo : 'skip' ,
899+ } )
908900 } )
909- } )
910- }
911901
912- // Secondary stddev region (only for first device)
913- // Split into segments at gaps (null values) so Plotly doesn't interpolate across gaps
914- if ( secondaryConfig && ! isRawData && deviceData . length > 0 ) {
915- const d = deviceData [ 0 ]
916- const bandTimestamps = hasSmoothing ? d . smoothedTimestamps : d . timestamps
917- const bandUpper = hasSmoothing ? d . secondaryUpperValues : d . secondaryAvgValues . map ( ( avg , i ) =>
918- avg === null || d . secondaryStddevValues [ i ] === null ? null : avg + d . secondaryStddevValues [ i ]
919- )
920- const bandLower = hasSmoothing ? d . secondaryLowerValues : d . secondaryAvgValues . map ( ( avg , i ) =>
921- avg === null || d . secondaryStddevValues [ i ] === null ? null : avg - d . secondaryStddevValues [ i ]
922- )
923-
924- // Split data into segments at null values
925- const segments : Array < { timestamps : string [ ] ; upper : ( number | null ) [ ] ; lower : ( number | null ) [ ] } > = [ ]
926- let currentSegment : { timestamps : string [ ] ; upper : ( number | null ) [ ] ; lower : ( number | null ) [ ] } | null = null
902+ // Secondary stddev bands
903+ if ( secondaryConfig ) {
904+ const secBandTimestamps = hasSmoothing ? d . smoothedTimestamps : d . timestamps
905+ const secBandUpper = hasSmoothing ? d . secondaryUpperValues : d . secondaryAvgValues . map ( ( avg , i ) =>
906+ avg === null || d . secondaryStddevValues [ i ] === null ? null : avg + d . secondaryStddevValues [ i ]
907+ )
908+ const secBandLower = hasSmoothing ? d . secondaryLowerValues : d . secondaryAvgValues . map ( ( avg , i ) =>
909+ avg === null || d . secondaryStddevValues [ i ] === null ? null : avg - d . secondaryStddevValues [ i ]
910+ )
927911
928- for ( let i = 0 ; i < bandTimestamps . length ; i ++ ) {
929- if ( bandUpper [ i ] === null || bandLower [ i ] === null ) {
930- if ( currentSegment && currentSegment . timestamps . length > 0 ) {
931- segments . push ( currentSegment )
932- }
933- currentSegment = null
934- } else {
935- if ( ! currentSegment ) {
936- currentSegment = { timestamps : [ ] , upper : [ ] , lower : [ ] }
937- }
938- currentSegment . timestamps . push ( bandTimestamps [ i ] )
939- currentSegment . upper . push ( bandUpper [ i ] )
940- currentSegment . lower . push ( bandLower [ i ] )
912+ const secSegments = splitBandSegments ( secBandTimestamps , secBandUpper , secBandLower )
913+ secSegments . forEach ( ( seg , segIdx ) => {
914+ traces . push ( {
915+ x : seg . timestamps ,
916+ y : seg . lower ,
917+ mode : 'lines' ,
918+ line : { color : 'transparent' } ,
919+ name : `${ secondaryConfig . label } Lower` ,
920+ showlegend : false ,
921+ hoverinfo : 'skip' ,
922+ yaxis : 'y2' ,
923+ } )
924+ traces . push ( {
925+ x : seg . timestamps ,
926+ y : seg . upper ,
927+ fill : 'tonexty' ,
928+ fillcolor : `${ d . secondaryLineProps ?. color } ${ opacityHex } ` ,
929+ line : { color : 'transparent' } ,
930+ mode : 'lines' ,
931+ name : segIdx === 0 ? `±σ ${ secondaryConfig . label } ` : `±σ ${ secondaryConfig . label } (cont)` ,
932+ showlegend : false ,
933+ hoverinfo : 'skip' ,
934+ yaxis : 'y2' ,
935+ } )
936+ } )
941937 }
942- }
943- if ( currentSegment && currentSegment . timestamps . length > 0 ) {
944- segments . push ( currentSegment )
945- }
946-
947- // Create traces for each segment
948- segments . forEach ( ( seg , segIdx ) => {
949- traces . push ( {
950- x : seg . timestamps ,
951- y : seg . lower ,
952- mode : 'lines' ,
953- line : { color : 'transparent' } ,
954- name : `${ secondaryConfig . label } Lower` ,
955- showlegend : false ,
956- hoverinfo : 'skip' ,
957- yaxis : 'y2' ,
958- } )
959- traces . push ( {
960- x : seg . timestamps ,
961- y : seg . upper ,
962- fill : 'tonexty' ,
963- fillcolor : `${ d . secondaryLineProps ?. color } ${ opacityHex } ` ,
964- line : { color : 'transparent' } ,
965- mode : 'lines' ,
966- name : `±σ ${ secondaryConfig . label } ` ,
967- showlegend : false ,
968- hoverinfo : 'skip' ,
969- yaxis : 'y2' ,
970- } )
971938 } )
972939 }
973940
0 commit comments