@@ -24,21 +24,34 @@ const PADDING = { top: 20, right: 20, bottom: 40, left: 65 };
2424
2525export function buildSmoothPath ( points : Point [ ] ) : string {
2626 if ( points . length === 0 ) return "" ;
27- if ( points . length === 1 ) return `M ${ points [ 0 ] . x } ,${ points [ 0 ] . y } ` ;
28-
29- const commands = [ `M ${ points [ 0 ] . x } ,${ points [ 0 ] . y } ` ] ;
30- for ( let i = 0 ; i < points . length - 1 ; i ++ ) {
31- const previous = points [ i - 1 ] ?? points [ i ] ;
32- const current = points [ i ] ;
33- const next = points [ i + 1 ] ;
34- const following = points [ i + 2 ] ?? next ;
27+
28+ // A path must not revisit an x coordinate. This is defensive for callers
29+ // that receive duplicate buckets; the history API also de-duplicates them.
30+ const uniquePoints = [ ...points ]
31+ . sort ( ( a , b ) => a . x - b . x )
32+ . filter ( ( point , index , sorted ) => index === 0 || point . x !== sorted [ index - 1 ] . x ) ;
33+ if ( uniquePoints . length === 1 ) return `M ${ uniquePoints [ 0 ] . x } ,${ uniquePoints [ 0 ] . y } ` ;
34+
35+ const minY = Math . min ( ...uniquePoints . map ( ( point ) => point . y ) ) ;
36+ const maxY = Math . max ( ...uniquePoints . map ( ( point ) => point . y ) ) ;
37+ const clampY = ( y : number ) => Math . min ( maxY , Math . max ( minY , y ) ) ;
38+
39+ const commands = [ `M ${ uniquePoints [ 0 ] . x } ,${ uniquePoints [ 0 ] . y } ` ] ;
40+ for ( let i = 0 ; i < uniquePoints . length - 1 ; i ++ ) {
41+ const previous = uniquePoints [ i - 1 ] ?? uniquePoints [ i ] ;
42+ const current = uniquePoints [ i ] ;
43+ const next = uniquePoints [ i + 1 ] ;
44+ const following = uniquePoints [ i + 2 ] ?? next ;
45+ const segmentWidth = next . x - current . x ;
3546 const control1 = {
36- x : current . x + ( next . x - previous . x ) / 6 ,
37- y : current . y + ( next . y - previous . y ) / 6 ,
47+ // Keep both controls inside this segment so x remains monotonic even
48+ // when valid hit-rate points are separated by empty buckets.
49+ x : current . x + segmentWidth / 3 ,
50+ y : clampY ( current . y + ( next . y - previous . y ) / 6 ) ,
3851 } ;
3952 const control2 = {
40- x : next . x - ( following . x - current . x ) / 6 ,
41- y : next . y - ( following . y - current . y ) / 6 ,
53+ x : next . x - segmentWidth / 3 ,
54+ y : clampY ( next . y - ( following . y - current . y ) / 6 ) ,
4255 } ;
4356 commands . push ( `C ${ control1 . x } ,${ control1 . y } ${ control2 . x } ,${ control2 . y } ${ next . x } ,${ next . y } ` ) ;
4457 }
0 commit comments