@@ -110,6 +110,37 @@ map.addControl(new L.Control.BackgroundMenu());
110110// Add search menu control to map
111111map . addControl ( new L . Control . SearchMenu ( ) ) ;
112112
113+ // Add chart toggle control
114+ L . Control . ChartToggle = L . Control . extend ( {
115+ options : {
116+ position : 'topright'
117+ } ,
118+
119+ onAdd : function ( map ) {
120+ const container = L . DomUtil . create ( 'div' , 'leaflet-control-chart-toggle' ) ;
121+ const button = L . DomUtil . create ( 'button' , 'leaflet-control-search-btn' , container ) ;
122+
123+ button . innerHTML = '📈' ;
124+ button . title = 'Toggle Chart' ;
125+ button . onclick = ( ) => {
126+ const chartContainer = document . querySelector ( '.chart-container' ) ;
127+ if ( chartContainer . style . display === 'none' || chartContainer . style . display === '' ) {
128+ chartContainer . style . display = 'block' ;
129+ if ( typeof chart !== 'undefined' && chart ) {
130+ chart . resize ( ) ;
131+ }
132+ } else {
133+ chartContainer . style . display = 'none' ;
134+ }
135+ } ;
136+
137+ return container ;
138+ }
139+ } ) ;
140+
141+ // Add chart toggle control to map
142+ map . addControl ( new L . Control . ChartToggle ( ) ) ;
143+
113144// Add screenshot control
114145L . Control . Screenshot = L . Control . extend ( {
115146 options : {
@@ -165,12 +196,17 @@ L.Control.Screenshot = L.Control.extend({
165196 document . body . appendChild ( loadingDiv ) ;
166197
167198 // Capture the map
168- html2canvas ( document . getElementById ( 'map ' ) , {
199+ html2canvas ( document . getElementById ( 'app-container ' ) , {
169200 useCORS : true ,
170201 allowTaint : false ,
171202 scale : 2 , // Higher resolution
172203 width : window . innerWidth ,
173- height : window . innerHeight
204+ height : window . innerHeight ,
205+ ignoreElements : ( element ) => {
206+ // Ignore the loading indicator if it's inside app-container (it shouldn't be, but just in case)
207+ // Also ignore data selector if it's hidden (html2canvas usually handles hidden, but let's be safe)
208+ return element . classList . contains ( 'data-selector' ) && element . style . display === 'none' ;
209+ }
174210 } ) . then ( canvas => {
175211 // Remove loading indicator
176212 document . body . removeChild ( loadingDiv ) ;
@@ -334,6 +370,7 @@ L.Control.GIFRecorder = L.Control.extend({
334370 const gif = new GIF ( {
335371 workers : 2 ,
336372 quality : 10 ,
373+ dither : "FloydSteinberg" ,
337374 width : map . getSize ( ) . x ,
338375 height : map . getSize ( ) . y ,
339376 workerScript : workerUrl
@@ -397,11 +434,16 @@ L.Control.GIFRecorder = L.Control.extend({
397434 await new Promise ( resolve => setTimeout ( resolve , 200 ) ) ;
398435
399436 try {
400- const canvas = await html2canvas ( document . getElementById ( 'map ' ) , {
437+ const canvas = await html2canvas ( document . getElementById ( 'app-container ' ) , {
401438 useCORS : true ,
402439 allowTaint : false ,
403440 logging : false ,
404- scale : 1 // Use 1 for GIF to keep size reasonable
441+ scale : 1 , // Use 1 for GIF to keep size reasonable
442+ ignoreElements : ( element ) => {
443+ // Ignore loading div if it somehow gets in there (it's in body, so fine)
444+ // Ignore hidden elements
445+ return element . style . display === 'none' ;
446+ }
405447 } ) ;
406448
407449 const currentFps = parseFloat ( document . getElementById ( 'fpsInput' ) . value ) || 10 ;
@@ -686,10 +728,11 @@ L.svg().addTo(map);
686728const overlay = d3 . select ( map . getPanes ( ) . overlayPane ) . select ( "svg" ) ;
687729const g = overlay . append ( "g" ) . attr ( "class" , "leaflet-zoom-hide" ) ;
688730
689- let edges , densities ;
731+ let edges , densities , globalData ;
690732let timeStamp = new Date ( ) ;
691733let highlightedEdge = null ;
692734let highlightedNode = null ;
735+ let chart ;
693736
694737function formatTime ( date ) {
695738 const year = date . getFullYear ( ) ;
@@ -770,24 +813,25 @@ loadDataBtn.addEventListener('click', async function() {
770813 // Fetch CSV files from the data subdirectory
771814 const edgesUrl = `../${ dirName } /edges.csv` ;
772815 const densitiesUrl = `../${ dirName } /densities.csv` ;
816+ const dataUrl = `../${ dirName } /data.csv` ;
773817
774818 // Load CSV data
775819 Promise . all ( [
776820 d3 . dsv ( ";" , edgesUrl , parseEdges ) ,
777- d3 . dsv ( ";" , densitiesUrl , parseDensity )
778- ] ) . then ( ( [ edgesData , densityData ] ) => {
821+ d3 . dsv ( ";" , densitiesUrl , parseDensity ) ,
822+ d3 . dsv ( ";" , dataUrl , parseData ) . catch ( e => { console . warn ( 'data.csv not found or invalid' , e ) ; return [ ] ; } )
823+ ] ) . then ( ( [ edgesData , densityData , additionalData ] ) => {
779824 edges = edgesData ;
780825 densities = densityData ;
826+ globalData = additionalData ;
781827
782828 // console.log("Edges:", edges);
783829 // console.log("Densities:", densities);
784830
785831 if ( ! edges . length || ! densities . length ) {
786832 console . error ( "Missing CSV data." ) ;
787833 return ;
788- }
789-
790- timeStamp = densities [ 0 ] . datetime ;
834+ } timeStamp = densities [ 0 ] . datetime ;
791835
792836 // Calculate median center from edge geometries
793837 let allLats = [ ] ;
@@ -812,13 +856,124 @@ loadDataBtn.addEventListener('click', async function() {
812856 const canvasEdges = new L . CanvasEdges ( edges ) ;
813857 canvasEdges . addTo ( map ) ;
814858
859+ let currentChartColumn = 'mean_density_vpk' ;
860+
861+ // Initialize Chart
862+ if ( globalData && globalData . length > 0 ) {
863+ const columns = Object . keys ( globalData [ 0 ] ) . filter ( k => k !== 'datetime' ) ;
864+ const selector = document . getElementById ( 'chartColumnSelector' ) ;
865+ selector . innerHTML = '' ;
866+ columns . forEach ( col => {
867+ const option = document . createElement ( 'option' ) ;
868+ option . value = col ;
869+ option . text = col ;
870+ selector . appendChild ( option ) ;
871+ } ) ;
872+
873+ if ( columns . includes ( 'mean_density_vpk' ) ) {
874+ selector . value = 'mean_density_vpk' ;
875+ } else if ( columns . length > 0 ) {
876+ selector . value = columns [ 0 ] ;
877+ }
878+ currentChartColumn = selector . value ;
879+
880+ selector . onchange = ( ) => {
881+ currentChartColumn = selector . value ;
882+ initChart ( ) ;
883+ updateChart ( ) ;
884+ } ;
885+
886+ initChart ( ) ;
887+ // document.querySelector('.chart-container').style.display = 'block';
888+ }
889+
890+ function initChart ( ) {
891+ const ctx = document . getElementById ( 'densityChart' ) . getContext ( '2d' ) ;
892+ if ( chart ) chart . destroy ( ) ;
893+
894+ chart = new Chart ( ctx , {
895+ type : 'line' ,
896+ data : {
897+ labels : globalData . map ( d => formatTime ( d . datetime ) ) ,
898+ datasets : [ {
899+ label : currentChartColumn ,
900+ data : globalData . map ( d => d [ currentChartColumn ] ) ,
901+ borderColor : 'blue' ,
902+ borderWidth : 1 ,
903+ pointRadius : 0 ,
904+ fill : false ,
905+ tension : 0.1
906+ } , {
907+ label : 'Current Time' ,
908+ data : [ ] , // Will be populated dynamically
909+ borderColor : 'red' ,
910+ backgroundColor : 'red' ,
911+ pointRadius : 5 ,
912+ pointHoverRadius : 7 ,
913+ showLine : false
914+ } ]
915+ } ,
916+ options : {
917+ responsive : true ,
918+ maintainAspectRatio : false ,
919+ animation : {
920+ duration : 0
921+ } ,
922+ scales : {
923+ x : {
924+ display : true ,
925+ title : {
926+ display : true ,
927+ text : 'time'
928+ } ,
929+ ticks : {
930+ display : false
931+ }
932+ } ,
933+ y : {
934+ beginAtZero : true ,
935+ title : {
936+ display : true ,
937+ text : currentChartColumn
938+ }
939+ }
940+ } ,
941+ plugins : {
942+ legend : {
943+ display : true ,
944+ labels : {
945+ boxWidth : 10
946+ }
947+ }
948+ }
949+ }
950+ } ) ;
951+ }
952+
953+ function updateChart ( ) {
954+ if ( ! chart || ! globalData ) return ;
955+
956+ // Find current data point index based on timeStamp
957+ const currentIndex = densities . findIndex ( d => d . datetime . getTime ( ) === timeStamp . getTime ( ) ) ;
958+
959+ if ( currentIndex !== - 1 ) {
960+ const pointData = new Array ( globalData . length ) . fill ( null ) ;
961+ if ( globalData [ currentIndex ] ) {
962+ pointData [ currentIndex ] = globalData [ currentIndex ] [ currentChartColumn ] ;
963+ }
964+ chart . data . datasets [ 1 ] . data = pointData ;
965+ chart . update ( 'none' ) ;
966+ }
967+ }
968+
815969 // Function to update edge positions, and color edges based on density
816970 function update ( ) {
817971 // Update edge stroke width based on zoom level (handled in Canvas layer)
818972 // No need to update paths, Canvas layer handles it
819973
820974 updateDensityVisualization ( ) ;
821975 updateNodeHighlight ( ) ;
976+ updateChart ( ) ;
822977 }
823978
824979 map . on ( "zoomend" , update ) ;
@@ -1091,4 +1246,15 @@ function parseDensity(d) {
10911246 return val === "" ? 0 : + val ;
10921247 } ) ;
10931248 return { datetime, densities } ;
1249+ }
1250+
1251+ // Parsing function for data CSV
1252+ function parseData ( d ) {
1253+ const result = { datetime : new Date ( d . datetime ) } ;
1254+ for ( const key in d ) {
1255+ if ( key !== 'datetime' ) {
1256+ result [ key ] = + d [ key ] ;
1257+ }
1258+ }
1259+ return result ;
10941260}
0 commit comments