@@ -416,21 +416,17 @@ export function App() {
const handleZoomIn = ( ) = > {
if ( ! fullChartData . length ) return ;
// Keep current X range, only adjust Y max
const currentXStart = zoomState ? . xStart ? ? fullChartData [ 0 ] . timestamp ;
const currentXEnd = zoomState ? . xEnd ? ? fullChartData [ fullChartData . length - 1 ] . timestamp ;
const currentYMax = zoomState ? . yMax ? ? yAxisDomain [ 1 ] ;
const xRange = currentXEnd - currentXStart ;
const newXRange = xRange * 0.8 ;
const xCenter = ( currentXStart + currentXEnd ) / 2 ;
// Y-axis: zoom from 0
// Y-axis: decrease max to zoom in (show less range)
const newYMax = currentYMax * 0.8 ;
setZoomState ( {
xStart : xCenter - newXRange / 2 ,
xEnd : xCenter + newXRange / 2 ,
xStart : currentXStart ,
xEnd : currentXEnd ,
yMin : 0 ,
yMax : newYMax ,
} ) ;
@@ -439,29 +435,83 @@ export function App() {
const handleZoomOut = ( ) = > {
if ( ! fullChartData . length ) return ;
// Keep current X range, only adjust Y max
const currentXStart = zoomState ? . xStart ? ? fullChartData [ 0 ] . timestamp ;
const currentXEnd = zoomState ? . xEnd ? ? fullChartData [ fullChartData . length - 1 ] . timestamp ;
const currentYMax = zoomState ? . yMax ? ? yAxisDomain [ 1 ] ;
// Y-axis: increase max to zoom out (show more range)
const newYMax = currentYMax * 1.25 ;
setZoomState ( {
xStart : currentXStart ,
xEnd : currentXEnd ,
yMin : 0 ,
yMax : newYMax ,
} ) ;
} ;
const handleTimelineCompress = ( ) = > {
if ( ! fullChartData . length ) return ;
const currentXStart = zoomState ? . xStart ? ? fullChartData [ 0 ] . timestamp ;
const currentXEnd = zoomState ? . xEnd ? ? fullChartData [ fullChartData . length - 1 ] . timestamp ;
const currentYMax = zoomState ? . yMax ? ? yAxisDomain [ 1 ] ;
const xRange = currentXEnd - currentXStart ;
const newXRange = xRange * 1.25 ;
const newXRange = xRange * 0.8 ; // Compress = show less time
const xCenter = ( currentXStart + currentXEnd ) / 2 ;
// Constrain to data bounds
const dataXStart = fullChartData [ 0 ] . timestamp ;
const dataXEnd = fullChartData [ fullChartData . length - 1 ] . timestamp ;
let newXStart = xCenter - newXRange / 2 ;
let newXEnd = xCenter + newXRange / 2 ;
// Ensure we stay within data bounds
if ( newXStart < dataXStart ) {
newXEnd += dataXStart - newXStart ;
newXStart = dataXStart ;
}
if ( newXEnd > dataXEnd ) {
newXStart -= newXEnd - dataXEnd ;
newXEnd = dataXEnd ;
}
newXStart = Math . max ( dataXStart , newXStart ) ;
newXEnd = Math . min ( dataXEnd , newXEnd ) ;
setZoomState ( {
xStart : newXStart ,
xEnd : newXEnd ,
yMin : 0 ,
yMax : currentYMax ,
} ) ;
} ;
const handleTimelineExpand = ( ) = > {
if ( ! fullChartData . length ) return ;
const currentXStart = zoomState ? . xStart ? ? fullChartData [ 0 ] . timestamp ;
const currentXEnd = zoomState ? . xEnd ? ? fullChartData [ fullChartData . length - 1 ] . timestamp ;
const currentYMax = zoomState ? . yMax ? ? yAxisDomain [ 1 ] ;
const xRange = currentXEnd - currentXStart ;
const newXRange = xRange * 1.25 ; // Expand = show more time
const xCenter = ( currentXStart + currentXEnd ) / 2 ;
// Constrain to data bounds
const dataXStart = fullChartData [ 0 ] . timestamp ;
const dataXEnd = fullChartData [ fullChartData . length - 1 ] . timestamp ;
const newXStart = Math . max ( dataXStart , xCenter - newXRange / 2 ) ;
const newXEnd = Math . min ( dataXEnd , xCenter + newXRange / 2 ) ;
// Y-axis: zoom from 0
const newYMax = currentYMax * 1.25 ;
setZoomState ( {
xStart : newXStart ,
xEnd : newXEnd ,
yMin : 0 ,
yMax : newYMax ,
yMax : currentYMax ,
} ) ;
} ;
@@ -474,6 +524,63 @@ export function App() {
return fullXRange / currentXRange ;
} ;
// Timeline slider position (0-100)
const sliderPosition = useMemo ( ( ) = > {
if ( ! fullChartData . length ) return 50 ;
const dataXStart = fullChartData [ 0 ] . timestamp ;
const dataXEnd = fullChartData [ fullChartData . length - 1 ] . timestamp ;
const fullRange = dataXEnd - dataXStart ;
if ( ! zoomState || fullRange === 0 ) return 50 ;
const viewCenter = ( zoomState . xStart + zoomState . xEnd ) / 2 ;
const position = ( ( viewCenter - dataXStart ) / fullRange ) * 100 ;
return Math . max ( 0 , Math . min ( 100 , position ) ) ;
} , [ fullChartData , zoomState ] ) ;
const handleSliderChange = ( e : React.ChangeEvent < HTMLInputElement > ) = > {
if ( ! fullChartData . length ) return ;
const position = parseFloat ( e . target . value ) ;
const dataXStart = fullChartData [ 0 ] . timestamp ;
const dataXEnd = fullChartData [ fullChartData . length - 1 ] . timestamp ;
const fullRange = dataXEnd - dataXStart ;
// Calculate new center based on slider position
const newCenter = dataXStart + ( position / 100 ) * fullRange ;
// Keep current view width or use full range if no zoom
const currentXStart = zoomState ? . xStart ? ? dataXStart ;
const currentXEnd = zoomState ? . xEnd ? ? dataXEnd ;
const currentYMax = zoomState ? . yMax ? ? yAxisDomain [ 1 ] ;
const viewWidth = currentXEnd - currentXStart ;
// Calculate new bounds centered on slider position
let newXStart = newCenter - viewWidth / 2 ;
let newXEnd = newCenter + viewWidth / 2 ;
// Constrain to data bounds
if ( newXStart < dataXStart ) {
newXEnd += dataXStart - newXStart ;
newXStart = dataXStart ;
}
if ( newXEnd > dataXEnd ) {
newXStart -= newXEnd - dataXEnd ;
newXEnd = dataXEnd ;
}
newXStart = Math . max ( dataXStart , newXStart ) ;
newXEnd = Math . min ( dataXEnd , newXEnd ) ;
setZoomState ( {
xStart : newXStart ,
xEnd : newXEnd ,
yMin : 0 ,
yMax : currentYMax ,
} ) ;
} ;
return (
< div style = { {
fontFamily : 'system-ui' ,
@@ -716,6 +823,38 @@ export function App() {
>
Reset ( × { getZoomLevel ( ) . toFixed ( 1 ) } )
< / button >
< button
onClick = { handleTimelineExpand }
style = { {
padding : '6px 12px' ,
backgroundColor : '#2a2f4a' ,
color : '#fff' ,
border : '1px solid #50e3c2' ,
borderRadius : '4px' ,
cursor : 'pointer' ,
fontSize : '14px' ,
fontWeight : 'bold' ,
} }
title = "Expand Timeline"
>
& lt ;
< / button >
< button
onClick = { handleTimelineCompress }
style = { {
padding : '6px 12px' ,
backgroundColor : '#2a2f4a' ,
color : '#fff' ,
border : '1px solid #50e3c2' ,
borderRadius : '4px' ,
cursor : 'pointer' ,
fontSize : '14px' ,
fontWeight : 'bold' ,
} }
title = "Compress Timeline"
>
& gt ;
< / button >
< / div >
< div style = { { color : '#888' , fontSize : '11px' , fontStyle : 'italic' } } >
@@ -789,6 +928,37 @@ export function App() {
< / LineChart >
< / ResponsiveContainer >
< / div >
{ /* Timeline Slider */ }
< div style = { {
marginTop : '15px' ,
padding : '10px 15px' ,
backgroundColor : '#0f1329' ,
borderRadius : '4px' ,
border : '1px solid #2a2f4a' ,
} } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : '15px' } } >
< span style = { { color : '#888' , fontSize : '12px' , minWidth : '60px' } } >
{ fullChartData . length > 0 ? new Date ( fullChartData [ 0 ] . timestamp ) . toLocaleDateString ( ) : '' }
< / span >
< input
type = "range"
min = "0"
max = "100"
value = { sliderPosition }
onChange = { handleSliderChange }
style = { {
flex : 1 ,
height : '8px' ,
cursor : 'pointer' ,
accentColor : '#50e3c2' ,
} }
/ >
< span style = { { color : '#888' , fontSize : '12px' , minWidth : '60px' , textAlign : 'right' } } >
{ fullChartData . length > 0 ? new Date ( fullChartData [ fullChartData . length - 1 ] . timestamp ) . toLocaleDateString ( ) : '' }
< / span >
< / div >
< / div >
< / div >
) : (
< div style = { { textAlign : 'center' , padding : '40px' , color : '#ff6b9d' } } >
@@ -854,7 +1024,7 @@ export function App() {
< th style = { { textAlign : 'right' , padding : '12px' , color : '#888' , fontWeight : 'normal' } } > Price / 1 M AUEC < / th >
{ customAuecAmount && (
< th style = { { textAlign : 'right' , padding : '12px' , color : '#888' , fontWeight : 'normal' } } >
Price for { ( customAuecAmount / 1000000 ) . toLocaleString ( ) } M AUEC
Price for { customAuecAmount . toLocaleString ( ) } AUEC
< / th >
) }
< / tr >
@@ -928,10 +1098,7 @@ export function App() {
) }
{ /* Alert Audio */ }
< audio
ref = { alertAudioRef }
src = "data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuBzvLZiTYIGGS57OihUBELTKXh8bllHAU2jdXvz3oqBSh+zPDajzsKElyx6OyrWBUIQ5zd8sFuJAUuhM/z24k2CBhku+zooVARC0yl4fG5ZRwFNo3V7896KgUofszw2o87ChJcsevsq1gVCEOc3fLBbiQFLoTP89uJNggYZLvs6KFQEQtMpeHxuWUcBTaN1e/PeioFKH7M8NqPOwsSXLHr7KtYFQhDnN3ywW4kBS6Ez/PbiTYIGGS77OihUBELTKXh8bllHAU2jdXvz3oqBSh+zPDajzsKElyx6+yrWBUIQ5zd8sFuJAUuhM/z24k2CBhku+zooVARC0yl4fG5ZRwFNo3V7896KgUofszw2o87ChJcsevsq1gVCEOc3fLBbiQFLoTP89uJNggYZLvs6KFQEQtMpeHxuWUcBTaN1e/PeioFKH7M8NqPOwsSXLHr7KtYFQhDnN3ywW4kBS6Ez/PbiTYIGGS77OihUBELTKXh8bllHAU2jdXvz3oqBSh+zPDajzsKElyx6+yrWBUIQ5zd8sFuJAUuhM/z24k2CBhku+zooVARC0yl4fG5ZRwFNo3V7896KgUofszw2o87ChJcsevsq1gVCEOc3fLBbiQFLoTP89uJNggYZLvs6KFQEQtMpeHxuWUcBTaN1e/PeioFKH7M8NqPOwsSXLHr7KtYFQhDnN3ywW4kBS6Ez/PbiTYIGGS77OihUBELTKXh8bllHAU2jdXvz3oqBSh+zPDajzsKElyx6+yrWBUIQ5zd8sFuJAUuhM/z24k2CBhku+zooVARC0yl4fG5ZRwFNo3V7896KgUofszw2o87ChJcsevsq1gVCEOc3fLBbiQFLoTP89uJNggYZLvs6KFQEQtMpeHxuWUcBTaN1e/PeioFKH7M8NqPOwsSXLHr7KtYFQhDnN3ywW4kBS6Ez/PbiTYIGGS77OihUBELTKXh8bllHAU2jdXvz3oqBSh+zPDajzsKElyx6+yrWBUIQ5zd8sFuJAUuhM/z24k2CBhku+zooVARC0yl4fG5ZRwFNo3V7896KgUofszw2o87ChJcsevsq1gVCEOc3fLBbiQFLoTP89uJNggYZLvs6KFQEQtMpeHxuWUcBTaN1e/PeioFKH7M8NqPOwsSXLHr7KtYFQhDnN3ywW4kBS6Ez/PbiTYIGGS77OihUBELTKXh8bllHAU2jdXvz3oqBSh+zPDajzsKElyx6+yrWBUIQ5zd8sFuJAUuhM/z24k2CBhku+zooVARC0yl4fG5ZRwFNo3V7896KgUofszw2o87ChJcsevsq1gVCEOc3fLBbiQFLoTP89uJNggYZLvs6KFQEQtMpeHxuWUcBTaN1e/PeioFKH7M8NqPOwsSXLHr7KtYFQhDnN3ywW4kBS6Ez/PbiTYIGGS77OihUBELTKXh8bllHAU2jdXvz3oqBSh+zPDajzsKElyx6+yrWBUIQ5zd8sFuJAUuhM/z24k2CBhku+zooVARC0yl4fG5ZRwFNo3V7896KgUofszw2o87ChJcsevsq1gVCEOc3fLBbiQFLoTP89uJNggYZLvs6KFQEQtMpeHxuWQ="
/ >
< audio ref = { alertAudioRef } src = "./notifcation.mp3" / >
< / div >
) ;
}