Skip to content

Commit 45a85b6

Browse files
committed
Add chart viewer and gif downloader
1 parent 7c96073 commit 45a85b6

File tree

3 files changed

+256
-54
lines changed

3 files changed

+256
-54
lines changed

webapp/index.html

Lines changed: 57 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,61 +21,75 @@
2121
<!-- gif.js for GIF generation -->
2222
<script src="https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js"></script>
2323

24+
<!-- Chart.js -->
25+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
26+
2427
<link rel="stylesheet" href="styles.css" />
2528
</head>
2629
<body>
30+
<div id="app-container">
31+
<!-- Map container -->
32+
<div id="map"></div>
2733

28-
<!-- Map container -->
29-
<div id="map"></div>
30-
31-
<!-- Data directory selector -->
32-
<div class="data-selector">
33-
<label for="dataDir">Data Directory Name:</label>
34-
<input type="text" id="dataDir" placeholder="Enter directory name (e.g., 'output')" />
35-
<button id="loadDataBtn">Load Data</button>
36-
<div class="data-info">
37-
<small>Place your edges and densities CSV files under the main directory: {directory_name}</small>
34+
<!-- Data directory selector -->
35+
<div class="data-selector">
36+
<label for="dataDir">Data Directory Name:</label>
37+
<input type="text" id="dataDir" placeholder="Enter directory name (e.g., 'output')" />
38+
<button id="loadDataBtn">Load Data</button>
39+
<div class="data-info">
40+
<small>Place your edges and densities CSV files under the main directory: {directory_name}</small>
41+
</div>
3842
</div>
39-
</div>
4043

41-
<!-- Search container -->
42-
<div class="search-container">
43-
<div>
44-
<label for="edgeSearch">Edge ID:</label>
45-
<input type="text" id="edgeSearch" />
46-
<button id="edgeSearchBtn">Search</button>
44+
<!-- Search container -->
45+
<div class="search-container">
46+
<div>
47+
<label for="edgeSearch">Edge ID:</label>
48+
<input type="text" id="edgeSearch" />
49+
<button id="edgeSearchBtn">Search</button>
50+
</div>
51+
<div>
52+
<label for="nodeSearch">Node ID:</label>
53+
<input type="text" id="nodeSearch" />
54+
<button id="nodeSearchBtn">Search</button>
55+
</div>
56+
<button id="inverseBtn" disabled>Inverse Edge</button>
57+
<button id="clearBtn">Clear Selections</button>
58+
<div id="searchResults"></div>
4759
</div>
48-
<div>
49-
<label for="nodeSearch">Node ID:</label>
50-
<input type="text" id="nodeSearch" />
51-
<button id="nodeSearchBtn">Search</button>
60+
61+
<!-- Slider for time step -->
62+
<div class="slider-container">
63+
<div class="slider-controls">
64+
<button id="playBtn" title="Play/Pause"></button>
65+
<div class="fps-control">
66+
<label for="fpsInput">FPS:</label>
67+
<input type="number" id="fpsInput" value="10" min="1" max="60" />
68+
</div>
69+
</div>
70+
<input type="range" id="timeSlider" min="0" step="300" value="0" />
71+
<span id="timeLabel">N/A</span>
5272
</div>
53-
<button id="inverseBtn" disabled>Inverse Edge</button>
54-
<button id="clearBtn">Clear Selections</button>
55-
<div id="searchResults"></div>
56-
</div>
5773

58-
<!-- Slider for time step -->
59-
<div class="slider-container">
60-
<div class="slider-controls">
61-
<button id="playBtn" title="Play/Pause"></button>
62-
<div class="fps-control">
63-
<label for="fpsInput">FPS:</label>
64-
<input type="number" id="fpsInput" value="10" min="1" max="60" />
74+
<!-- Legend container -->
75+
<div class="legend-container">
76+
<div class="legend-title">Density</div>
77+
<div class="legend-bar"></div>
78+
<div class="legend-labels">
79+
<span>0%</span>
80+
<span>50%</span>
81+
<span>100%</span>
6582
</div>
6683
</div>
67-
<input type="range" id="timeSlider" min="0" step="300" value="0" />
68-
<span id="timeLabel">N/A</span>
69-
</div>
7084

71-
<!-- Legend container -->
72-
<div class="legend-container">
73-
<div class="legend-title">Density</div>
74-
<div class="legend-bar"></div>
75-
<div class="legend-labels">
76-
<span>0%</span>
77-
<span>50%</span>
78-
<span>100%</span>
85+
<!-- Chart container -->
86+
<div class="chart-container">
87+
<div style="margin-bottom: 5px; height: 30px;">
88+
<select id="chartColumnSelector" style="width: 100%; padding: 5px;"></select>
89+
</div>
90+
<div style="position: relative; height: calc(100% - 40px); width: 100%;">
91+
<canvas id="densityChart"></canvas>
92+
</div>
7993
</div>
8094
</div>
8195

webapp/script.js

Lines changed: 176 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,37 @@ map.addControl(new L.Control.BackgroundMenu());
110110
// Add search menu control to map
111111
map.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
114145
L.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);
686728
const overlay = d3.select(map.getPanes().overlayPane).select("svg");
687729
const g = overlay.append("g").attr("class", "leaflet-zoom-hide");
688730

689-
let edges, densities;
731+
let edges, densities, globalData;
690732
let timeStamp = new Date();
691733
let highlightedEdge = null;
692734
let highlightedNode = null;
735+
let chart;
693736

694737
function 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

Comments
 (0)