@@ -13,15 +13,17 @@ document.addEventListener('DOMContentLoaded', async () => {
1313 acled : 'data/acled_by_oblast.json' ,
1414 aid : 'data/aid_by_country.json' ,
1515 timeline : 'data/timeline_events.json' ,
16- gdp : 'data/gdp_by_country.json'
16+ gdp : 'data/gdp_by_country.json' ,
17+ drone : 'data/drone_by_month.json' ,
1718 } ;
1819
1920 try {
20- const [ acledRaw , aidRaw , timelineRaw , gdpRaw ] = await Promise . all ( [
21+ const [ acledRaw , aidRaw , timelineRaw , gdpRaw , droneRaw ] = await Promise . all ( [
2122 d3 . json ( DATA_PATHS . acled ) ,
2223 d3 . json ( DATA_PATHS . aid ) ,
2324 d3 . json ( DATA_PATHS . timeline ) ,
24- d3 . json ( DATA_PATHS . gdp )
25+ d3 . json ( DATA_PATHS . gdp ) ,
26+ d3 . json ( DATA_PATHS . drone ) ,
2527 ] ) ;
2628
2729 // 1. Data Structure Normalization
@@ -43,6 +45,7 @@ document.addEventListener('DOMContentLoaded', async () => {
4345 renderAidDonut ( countriesArray ) ;
4446 renderRankingTable ( countriesArray , gdpRaw ) ;
4547 renderDeathTimeSeries ( acledRaw ) ;
48+ renderDroneTimeSeries ( droneRaw ) ;
4649
4750
4851 } catch ( err ) {
@@ -896,6 +899,84 @@ function renderDeathTimeSeries(acledRaw) {
896899 . attr ( "fill" , "var(--red)" ) ;
897900}
898901
902+ function renderDroneTimeSeries ( droneRaw ) {
903+ const container = d3 . select ( "#ts-drone-strikes" ) ;
904+ container . selectAll ( "*" ) . remove ( ) ;
905+
906+ const parseTime = d3 . timeParse ( "%Y-%m" ) ;
907+ const chartData = Object . entries ( droneRaw )
908+ . map ( ( [ month , count ] ) => ( { date : parseTime ( month ) , value : + count } ) )
909+ . filter ( d => d . date !== null )
910+ . sort ( ( a , b ) => a . date - b . date ) ;
911+
912+ if ( chartData . length === 0 ) return ;
913+
914+ const margin = { top : 20 , right : 30 , bottom : 50 , left : 55 } ;
915+ const width = container . node ( ) . getBoundingClientRect ( ) . width - margin . left - margin . right ;
916+ const height = 260 - margin . top - margin . bottom ;
917+
918+ const svg = container . append ( "svg" )
919+ . attr ( "width" , "100%" )
920+ . attr ( "height" , height + margin . top + margin . bottom )
921+ . attr ( "viewBox" , `0 0 ${ width + margin . left + margin . right } ${ height + margin . top + margin . bottom } ` )
922+ . append ( "g" )
923+ . attr ( "transform" , `translate(${ margin . left } ,${ margin . top } )` ) ;
924+
925+ const x = d3 . scaleTime ( ) . domain ( d3 . extent ( chartData , d => d . date ) ) . range ( [ 0 , width ] ) ;
926+ const y = d3 . scaleLinear ( ) . domain ( [ 0 , d3 . max ( chartData , d => d . value ) * 1.1 ] ) . range ( [ height , 0 ] ) ;
927+
928+ svg . append ( "g" )
929+ . attr ( "class" , "axis grid" )
930+ . attr ( "transform" , `translate(0,${ height } )` )
931+ . call ( d3 . axisBottom ( x ) . ticks ( 6 ) . tickFormat ( d3 . timeFormat ( "%Y/%m" ) ) . tickSize ( - height ) ) ;
932+
933+ svg . append ( "g" )
934+ . attr ( "class" , "axis grid" )
935+ . call ( d3 . axisLeft ( y ) . ticks ( 5 ) . tickSize ( - width ) ) ;
936+
937+ // Area fill
938+ svg . append ( "path" )
939+ . datum ( chartData )
940+ . attr ( "fill" , "rgba(52, 152, 219, 0.15)" )
941+ . attr ( "d" , d3 . area ( ) . x ( d => x ( d . date ) ) . y0 ( height ) . y1 ( d => y ( d . value ) ) . curve ( d3 . curveMonotoneX ) ) ;
942+
943+ svg . append ( "path" )
944+ . datum ( chartData )
945+ . attr ( "fill" , "none" )
946+ . attr ( "stroke" , "#3498db" )
947+ . attr ( "stroke-width" , 2.5 )
948+ . attr ( "d" , d3 . line ( ) . x ( d => x ( d . date ) ) . y ( d => y ( d . value ) ) . curve ( d3 . curveMonotoneX ) ) ;
949+
950+ let tooltip = d3 . select ( ".chart-tooltip" ) ;
951+ if ( tooltip . empty ( ) ) {
952+ tooltip = d3 . select ( "body" ) . append ( "div" ) . attr ( "class" , "chart-tooltip" ) . style ( "opacity" , 0 ) ;
953+ }
954+
955+ const mouseLine = svg . append ( "line" )
956+ . attr ( "y1" , 0 ) . attr ( "y2" , height )
957+ . attr ( "stroke" , "#aaa" ) . attr ( "stroke-width" , 1 ) . attr ( "stroke-dasharray" , "4" )
958+ . style ( "opacity" , 0 ) ;
959+
960+ svg . append ( "rect" )
961+ . attr ( "width" , width ) . attr ( "height" , height )
962+ . attr ( "fill" , "none" ) . attr ( "pointer-events" , "all" )
963+ . on ( "mouseout" , ( ) => { tooltip . style ( "opacity" , 0 ) ; mouseLine . style ( "opacity" , 0 ) ; } )
964+ . on ( "mousemove" , function ( event ) {
965+ const bisect = d3 . bisector ( d => d . date ) . left ;
966+ const date = x . invert ( d3 . pointer ( event ) [ 0 ] ) ;
967+ const i = bisect ( chartData , date , 1 ) ;
968+ const d0 = chartData [ i - 1 ] , d1 = chartData [ i ] ;
969+ const d = d1 && ( date - d0 . date > d1 . date - date ) ? d1 : d0 ;
970+ if ( ! d ) return ;
971+ mouseLine . attr ( "x1" , x ( d . date ) ) . attr ( "x2" , x ( d . date ) ) . style ( "opacity" , 1 ) ;
972+ tooltip . style ( "opacity" , 1 )
973+ . html ( `<span class="tooltip-date">${ d3 . timeFormat ( "%Y/%m" ) ( d . date ) } </span>
974+ <div><span style="color:#3498db">●</span> Strikes: <strong>${ d . value . toLocaleString ( ) } </strong></div>` )
975+ . style ( "left" , ( event . pageX + 15 ) + "px" )
976+ . style ( "top" , ( event . pageY - 28 ) + "px" ) ;
977+ } ) ;
978+ }
979+
899980
900981function renderCombinedAidDashboard ( countriesArray ) {
901982 const totalGlobalEur = d3 . sum ( countriesArray , d => d . total_eur || 0 ) ;
0 commit comments