1+ <!DOCTYPE html>
2+ < html lang ="en ">
3+ < head >
4+ < meta charset ="UTF-8 ">
5+ < title > Port Arrivals</ title >
6+ < link href ="https://fonts.googleapis.com/css2?family=Open+Sans&display=swap " rel ="stylesheet ">
7+ < script src ="https://cdn.plot.ly/plotly-2.27.0.min.js "> </ script >
8+ < script src ="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js "> </ script >
9+ < style >
10+ html , body {
11+ height : 100% ;
12+ margin-bottom : 5px ;
13+ padding : 2px ;
14+ font-family : 'Open Sans' , sans-serif;
15+ background-color : white;
16+ }
17+ h2 {
18+ font-size : 20px ;
19+ margin-bottom : 10px ;
20+ }
21+ # chart {
22+ width : 100% ;
23+ height : 500px ; /* or use 100vh for full screen */
24+ margin : 0 ;
25+ padding : 0 ;
26+ }
27+ label , select {
28+ font-size : 14px ;
29+ margin-right : 10px ;
30+ font-family : 'Open Sans' , sans-serif;
31+ }
32+ select {
33+ border : none;
34+ border-bottom : 1px solid # 888 ;
35+ background-color : transparent;
36+ font-size : 14px ;
37+ padding : 5px 0 ;
38+ outline : none;
39+ font-family : 'Open Sans' , sans-serif;
40+ color : # 333 ;
41+ cursor : pointer;
42+ }
43+ </ style >
44+ </ head >
45+ < body >
46+
47+ < div id ="titleRow " style ="
48+ margin-bottom: 10px;
49+ ">
50+ < h2 id ="chartTitle " style ="
51+ font-size: 20px;
52+ font-weight: bold;
53+ margin: 0;
54+ font-family: 'Open Sans', sans-serif;
55+ ">
56+ Port Arrivals Derived from AIS
57+ </ h2 >
58+ < p id ="chartSubtitle " style ="
59+ font-size: 16px;
60+ color: #666;
61+ margin: 4px 0 0;
62+ font-family: 'Open Sans', sans-serif;
63+ ">
64+ Counts, Monthly
65+ </ p >
66+ </ div >
67+
68+ < label for ="countrySelect "> Country:</ label >
69+ < select id ="countrySelect "> </ select >
70+
71+ < label for ="portSelect "> Port:</ label >
72+ < select id ="portSelect "> </ select >
73+
74+ < div id ="chart " style ="width:100%;position:relative; "> </ div >
75+ < div id ="footer " style ="
76+ width: 100%;
77+ display: flex;
78+ justify-content: space-between;
79+ align-items: center;
80+ font-family: 'Open Sans', sans-serif;
81+ font-size: 12px;
82+ color: #888;
83+ ">
84+ < div style ="margin-bottom:10px; ">
85+ Source: < a href ="https://www.worldbank.org " target ="_blank " style ="text-decoration: underline; color: #888; "> World Bank Group</ a >
86+ </ div >
87+ < img src ="https://public.flourish.studio/uploads/71a85b0c-7f8c-453b-bc80-22b945333d58.png " style ="height: 25px; object-fit: contain; margin-right: 5px; margin-bottom:10px; " alt ="Logo ">
88+ </ div >
89+
90+
91+
92+ < script >
93+ const hlCategories = [ "Cargo" , "Fishing" , "Passenger" , "Tanker" ] ;
94+ let fullData = [ ] ;
95+
96+ Papa . parse ( "https://raw.githubusercontent.com/cherrylchico/pacific-observatory-ais-data/main/data/monthly%20port%20calls.csv" , {
97+ download : true ,
98+ header : true ,
99+ skipEmptyLines : true ,
100+ complete : function ( results ) {
101+ fullData = results . data . filter ( d =>
102+ hlCategories . includes ( d . HL ) &&
103+ d [ "port call" ] &&
104+ ! isNaN ( parseInt ( d [ "port call" ] ) )
105+ ) ;
106+ populateCountryDropdown ( ) ;
107+ }
108+ } ) ;
109+
110+ function populateCountryDropdown ( ) {
111+ const countries = [ ...new Set ( fullData . map ( d => d . Country ) ) ] . sort ( ) ;
112+ const countrySelect = document . getElementById ( "countrySelect" ) ;
113+ countrySelect . innerHTML = "" ;
114+
115+ const allOption = document . createElement ( "option" ) ;
116+ allOption . value = "__ALL__" ;
117+ allOption . textContent = "All Countries" ;
118+ countrySelect . appendChild ( allOption ) ;
119+
120+ countries . forEach ( c => {
121+ const option = document . createElement ( "option" ) ;
122+ option . value = c ;
123+ option . textContent = c ;
124+ countrySelect . appendChild ( option ) ;
125+ } ) ;
126+
127+ countrySelect . addEventListener ( "change" , populatePortDropdown ) ;
128+ countrySelect . value = "__ALL__" ;
129+ populatePortDropdown ( ) ;
130+ }
131+
132+ function populatePortDropdown ( ) {
133+ const selectedCountry = document . getElementById ( "countrySelect" ) . value ;
134+ const portSelect = document . getElementById ( "portSelect" ) ;
135+
136+ portSelect . innerHTML = "" ;
137+
138+ const allOption = document . createElement ( "option" ) ;
139+ allOption . value = "__ALL__" ;
140+ allOption . textContent = "All Ports" ;
141+ portSelect . appendChild ( allOption ) ;
142+
143+ if ( selectedCountry === "__ALL__" ) {
144+ portSelect . disabled = true ;
145+ } else {
146+ const ports = [ ...new Set ( fullData . filter ( d => d . Country === selectedCountry ) . map ( d => d . Port ) ) ] . sort ( ) ;
147+ ports . forEach ( p => {
148+ const option = document . createElement ( "option" ) ;
149+ option . value = p ;
150+ option . textContent = p ;
151+ portSelect . appendChild ( option ) ;
152+ } ) ;
153+ portSelect . disabled = false ;
154+ }
155+
156+ portSelect . addEventListener ( "change" , updateChart ) ;
157+ portSelect . value = "__ALL__" ;
158+ updateChart ( ) ;
159+ }
160+
161+ function updateChart ( ) {
162+ const selectedCountry = document . getElementById ( "countrySelect" ) . value ;
163+ const selectedPort = document . getElementById ( "portSelect" ) . value ;
164+
165+ let filtered = fullData . filter ( d => hlCategories . includes ( d . HL ) ) ;
166+
167+ if ( selectedCountry !== "__ALL__" ) {
168+ filtered = filtered . filter ( d => d . Country === selectedCountry ) ;
169+ if ( selectedPort !== "__ALL__" ) {
170+ filtered = filtered . filter ( d => d . Port === selectedPort ) ;
171+ }
172+ }
173+
174+ const yearList = [ ...new Set ( filtered . map ( d => d . year ) ) ] . sort ( ) ;
175+ const allMonths = [ ] ;
176+ yearList . forEach ( year => {
177+ for ( let m = 1 ; m <= 12 ; m ++ ) {
178+ allMonths . push ( { year : year , month : m } ) ;
179+ }
180+ } ) ;
181+
182+ const traces = hlCategories . map ( hl => {
183+ const x = [ ] ;
184+ const y = [ ] ;
185+
186+ allMonths . forEach ( ( { year, month } ) => {
187+ const entries = filtered . filter ( d =>
188+ d . year == year &&
189+ parseInt ( d . month ) == month &&
190+ d . HL === hl
191+ ) ;
192+ const total = entries . reduce ( ( sum , d ) => sum + parseInt ( d [ "port call" ] ) , 0 ) ;
193+ if ( total > 0 ) {
194+ x . push ( `${ year } -${ month } ` ) ;
195+ y . push ( total ) ;
196+ }
197+ } ) ;
198+
199+ return {
200+ x : x ,
201+ y : y ,
202+ name : hl ,
203+ type : 'scatter' ,
204+ mode : 'lines'
205+ } ;
206+ } ) ;
207+
208+ const layout = {
209+ autosize : true ,
210+ margin : {
211+ l : 40 ,
212+ r : 20 ,
213+ t : 10 ,
214+ b : 0
215+ } ,
216+ yaxis : {
217+ showline :true ,
218+ linecolor : '#ccc' ,
219+ griddash : 'dot' ,
220+ gridcolor : '#ccc' ,
221+ ticks : 'outside' ,
222+ tickcolor : '#ccc' ,
223+ zeroline : false
224+ } ,
225+ xaxis : {
226+ rangeslider : { visible : true } ,
227+ title : '' ,
228+ showgrid : false ,
229+ ticks : 'outside' ,
230+ tickson : 'boundaries' ,
231+ tickcolor :'#ccc' ,
232+ showline : true ,
233+ linecolor : '#ccc'
234+ } ,
235+ legend : {
236+ orientation : 'h' ,
237+ x : 0 ,
238+ y : 1 ,
239+ } ,
240+ hoverlabel :{
241+ font :{ family : 'Open Sans, sans-serif' }
242+ } ,
243+ } ;
244+
245+ Plotly . newPlot ( 'chart' , traces , layout , { responsive : true } ) ;
246+
247+ const chartDiv = document . getElementById ( 'chart' ) ;
248+
249+ }
250+ </ script >
251+ </ body >
252+ </ html >
0 commit comments