├── .gitignore ├── images └── info.txt ├── requirements.txt ├── LICENSE ├── index.html ├── README.md ├── app.py ├── main.css └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | api_responses.json 2 | venv/ 3 | .vscode/ -------------------------------------------------------------------------------- /images/info.txt: -------------------------------------------------------------------------------- 1 | Place all image files in this folder, for maximum compatibility with app features. 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohappyeyeballs==2.3.4 2 | aiohttp==3.10.0 3 | aiosignal==1.3.1 4 | attrs==23.2.0 5 | frozenlist==1.4.1 6 | idna==3.7 7 | multidict==6.0.5 8 | yarl==1.9.4 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Bret Bernhoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GeoSpy API Mapping Application 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 |
27 | Image Preview 28 |
29 |

30 | Built By: 31 | Bret Bernhoft 32 |

33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GeoSpy API Mapping Application 2 | 3 | ![GeoSpy Web App Screenshot](https://hosting.photobucket.com/images/i/bernhoftbret/geospy-api-mapping-app-histogram-update.png) 4 | 5 | Query the [GeoSpy API](https://dev.geospy.ai/docs/routes#overview) for any allowed number of images using Python. Then visualize that JSON data on a world map with D3. 6 | 7 | ## Basic Usage 8 | 9 | After cloning the repo, first open the provided images directory, and add your files/photos therein. Then open the app.py script in a text editor. Here, enter your API_TOKEN value and IMAGE_FILES path(s), from the previously mentioned images directory. Once your details are saved, open a terminal and run `pip install -r requirements.txt`. Thereby ensuring that you have the Python library needed for this application to work properly. Following a successful install, run `python3 app.py` in the console. This will query the GeoSpy API, and produce a JSON file locally. 10 | 11 | Next, open the index.html file with any modern web browser. This will feature a world map, file uploader, "Load Data" button and other interfaces. Select your JSON with the file input element. Finally press the blue button to load and display the results. The predictive coordinates returned for each image will all share the same hue when viewed as markers on the world map. 12 | 13 | ## Please Read The Following 14 | 15 | This repo is no longer being actively maintained, given the GeoSpy API is not available for testing, demoing or generally playing around with. Best of luck if you, for some reason, do choose to use this application! 16 | 17 | If you have any questions, feel free to [reach out](https://bretbernhoft.com/). 18 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import asyncio 3 | import base64 4 | import logging 5 | import json 6 | from aiohttp import ClientTimeout 7 | 8 | logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s: %(message)s') 9 | 10 | API_TOKEN = "your_api_key" 11 | API_BASE = "https://dev.geospy.ai" 12 | API_ENDPOINT = "/predict" 13 | FULL_API_URL = f"{API_BASE}{API_ENDPOINT}" 14 | IMAGE_FILES = ["path/to/your/images/example.jpg",] 15 | 16 | request_timeout = ClientTimeout(total=60) 17 | 18 | collected_responses = [] 19 | 20 | async def convert_image_to_base64(image_path: str) -> str: 21 | with open(image_path, "rb") as file: 22 | encoded_string = base64.b64encode(file.read()).decode() 23 | return encoded_string 24 | 25 | async def post_image_data(session, encoded_image: str, file_path: str): 26 | data = { 27 | "inputs": {"image": encoded_image}, 28 | "top_k": 25, 29 | } 30 | headers = { 31 | 'Authorization': f'Bearer {API_TOKEN}', 32 | 'Content-Type': 'application/json', 33 | } 34 | 35 | try: 36 | async with session.post(FULL_API_URL, json=data, headers=headers, timeout=request_timeout) as resp: 37 | if resp.status == 200: 38 | response_data = await resp.json() 39 | collected_responses.append({file_path: response_data}) 40 | logging.debug(f"Request successful for {file_path}: {response_data}") 41 | else: 42 | logging.error(f"Request failed for {file_path} with status {resp.status}: {await resp.text()}") 43 | except Exception as error: 44 | logging.error(f"Error during request for {file_path}: {error}") 45 | 46 | async def process_images(): 47 | async with aiohttp.ClientSession() as session: 48 | tasks = [] 49 | for path in IMAGE_FILES: 50 | try: 51 | encoded_img = await convert_image_to_base64(path) 52 | task = asyncio.create_task(post_image_data(session, encoded_img, path)) 53 | tasks.append(task) 54 | await asyncio.sleep(1) 55 | except Exception as e: 56 | logging.error(f"Error processing image {path}: {e}") 57 | await asyncio.gather(*tasks, return_exceptions=True) 58 | with open('api_responses.json', 'w') as json_file: 59 | json.dump(collected_responses, json_file) 60 | 61 | if __name__ == "__main__": 62 | asyncio.run(process_images()) 63 | -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", Arial, sans-serif; 3 | margin: 0; 4 | padding: 20px; 5 | background-color: #f8f9fa; 6 | color: #333; 7 | line-height: 1.6; 8 | } 9 | h4 { 10 | margin: 0 0 15px 0; 11 | text-align: center; 12 | font-weight: 300; 13 | } 14 | h3 { 15 | margin: 0 0 10px 0; 16 | } 17 | #mapid { 18 | height: 400px; 19 | width: 100%; 20 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 21 | margin-bottom: 20px; 22 | border-radius: 5px; 23 | } 24 | .custom-icon { 25 | border-radius: 50%; 26 | } 27 | #addressList { 28 | min-height: 300px; 29 | max-height: 300px; 30 | overflow-y: auto; 31 | border: 1px solid #ddd; 32 | background-color: white; 33 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); 34 | padding: 10px; 35 | border-radius: 5px; 36 | margin-bottom: 20px; 37 | } 38 | .address-item { 39 | cursor: pointer; 40 | padding: 10px; 41 | margin-bottom: 10px; 42 | border-radius: 5px; 43 | transition: background-color 0.3s, transform 0.2s; 44 | border-left: 3px solid transparent; 45 | } 46 | .address-item:hover { 47 | background-color: #e9ecef; 48 | transform: translateX(5px); 49 | border-left: 3px solid #007bff; 50 | } 51 | .uiHolders { 52 | display: flex; 53 | align-items: center; 54 | justify-content: flex-start; 55 | margin-bottom: 20px; 56 | } 57 | .uiHolders button, 58 | .uiHolders select, 59 | .uiHolders input { 60 | margin-right: 10px; 61 | } 62 | input[type="file"], 63 | input[type="text"], 64 | button, 65 | select { 66 | display: block; 67 | margin: 0 10px 0 0; 68 | padding: 10px; 69 | border: 1px solid #ccc; 70 | border-radius: 5px; 71 | background-color: #fff; 72 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06); 73 | cursor: pointer; 74 | font-size: 16px; 75 | } 76 | button { 77 | color: white; 78 | background-color: #007bff; 79 | border-color: #007bff; 80 | transition: background-color 0.3s, border-color 0.3s; 81 | } 82 | button:hover { 83 | background-color: #0056b3; 84 | border-color: #004085; 85 | } 86 | #imagePreview { 87 | position: fixed; 88 | bottom: 20px; 89 | right: 20px; 90 | width: 400px; 91 | display: none; 92 | overflow: hidden; 93 | background-color: rgba(0, 0, 0, 0); 94 | } 95 | #previewImg { 96 | width: 100%; 97 | height: auto; 98 | object-fit: cover; 99 | } 100 | #legend { 101 | background-color: white; 102 | padding: 10px; 103 | border-radius: 5px; 104 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); 105 | } 106 | #devCredit { 107 | text-align: center; 108 | font-size: 13px; 109 | margin-top: 20px; 110 | font-style: italic; 111 | } 112 | a { 113 | color: #007bff; 114 | text-decoration: none; 115 | } 116 | a:hover { 117 | text-decoration: underline; 118 | } 119 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | var mymap = L.map("mapid").setView([27.7618, 0.3828], 2); 2 | L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { 3 | maxZoom: 19, 4 | attribution: "© OpenStreetMap contributors", 5 | }).addTo(mymap); 6 | var markers = []; 7 | var heatLayer; 8 | var addressElements = []; 9 | var initialData = []; 10 | var hideTimeout; 11 | function showImagePreview(src) { 12 | clearTimeout(hideTimeout); 13 | previewImg.src = src; 14 | imagePreview.style.display = "block"; 15 | } 16 | function hideImagePreview() { 17 | hideTimeout = setTimeout(function () { 18 | imagePreview.style.display = "none"; 19 | }, 500); 20 | } 21 | document 22 | .getElementById("imagePreview") 23 | .addEventListener("mouseover", function () { 24 | clearTimeout(hideTimeout); 25 | }); 26 | document 27 | .getElementById("imagePreview") 28 | .addEventListener("mouseout", function () { 29 | hideImagePreview(); 30 | }); 31 | function visualizeData(apiData) { 32 | document.getElementById("showHistogramButton").disabled = false; 33 | initialData = apiData; 34 | var addressList = document.getElementById("addressList"); 35 | addressList.innerHTML = ""; 36 | markers = []; 37 | var heatmapData = []; 38 | let minScore = Infinity; 39 | let maxScore = -Infinity; 40 | var baseHues = []; 41 | var baseHue; 42 | let addressElements = []; 43 | apiData.forEach(function (imageData) { 44 | Object.values(imageData).forEach(function (data) { 45 | data.geo_predictions.forEach(function (prediction) { 46 | minScore = Math.min(minScore, prediction.score); 47 | maxScore = Math.max(maxScore, prediction.score); 48 | }); 49 | }); 50 | }); 51 | apiData.forEach(function (imageData, objectIndex) { 52 | var imagePath = Object.keys(imageData)[0]; 53 | var fileName = imagePath.split("/").pop(); 54 | var serverRelativeImagePath = "images/" + fileName; 55 | 56 | let counter = 0; 57 | do { 58 | baseHue = Math.random() * 360; 59 | counter++; 60 | } while (baseHues.some((hue) => Math.abs(hue - baseHue) < 33) && counter < 50); 61 | baseHues.push(baseHue); 62 | var objectHue = baseHue % 360; 63 | Object.values(imageData).forEach(function (data) { 64 | data.geo_predictions.forEach(function (prediction, index) { 65 | var lat = prediction.coordinates[0]; 66 | var lon = prediction.coordinates[1]; 67 | var address = prediction.address; 68 | var color = `hsl(${objectHue}, 100%, 50%)`; 69 | var markerHtmlStyles = ` 70 | background-color: ${color}; 71 | width: 15px; 72 | height: 15px; 73 | display: block; 74 | position: relative; 75 | transform: translate(-25%, -25%); 76 | border-radius: 50%; 77 | border: 2px solid black; 78 | box-shadow: 0 0 13px rgba(0, 0, 0, 0.23); 79 | `; 80 | var markerColor = color; 81 | var icon = L.divIcon({ 82 | className: "custom-icon", 83 | iconAnchor: [5, 5], 84 | html: `
`, 85 | }); 86 | var marker = L.marker([lat, lon], { icon: icon }) 87 | .addTo(mymap) 88 | .bindTooltip( 89 | ` 90 |
91 | Filename: ${fileName}
92 | Address: ${address}
93 | Confidence: ${prediction.score} 94 |
95 | `, 96 | { 97 | permanent: false, 98 | direction: "auto", 99 | } 100 | ) 101 | .on("click", function () { 102 | mymap.setView([lat, lon], 19); 103 | }) 104 | .on("mouseover", function () { 105 | showImagePreview(serverRelativeImagePath); 106 | var iconElement = document.getElementById( 107 | `marker-${objectIndex}-${index}` 108 | ); 109 | if (iconElement) { 110 | iconElement.style.backgroundColor = "black"; 111 | } 112 | }) 113 | .on("mouseout", function () { 114 | hideImagePreview(); 115 | var iconElement = document.getElementById( 116 | `marker-${objectIndex}-${index}` 117 | ); 118 | if (iconElement) { 119 | iconElement.style.backgroundColor = markerColor; 120 | } 121 | }); 122 | markers.push({ 123 | marker, 124 | address, 125 | elementId: `marker-${objectIndex}-${index}`, 126 | }); 127 | 128 | var addressElement = document.createElement("div"); 129 | addressElement.innerHTML = `File Name: ${fileName}
130 | Address: ${address}
131 | Prediction Score: ${prediction.score}`; 132 | addressElement.className = "address-item"; 133 | addressElement.dataset.address = address.toLowerCase(); 134 | addressElement.dataset.fileName = fileName; 135 | addressElement.dataset.score = prediction.score; 136 | addressElement.dataset.markerId = `marker-${objectIndex}-${index}`; 137 | addressElement.marker = marker; 138 | addressElement.onmouseover = function () { 139 | showImagePreview(serverRelativeImagePath); 140 | this.marker.openTooltip(); 141 | var iconElement = document.getElementById( 142 | `marker-${objectIndex}-${index}` 143 | ); 144 | if (iconElement) { 145 | iconElement.style.backgroundColor = "black"; 146 | } 147 | }; 148 | addressElement.onmouseout = function () { 149 | hideImagePreview(); 150 | this.marker.closeTooltip(); 151 | var iconElement = document.getElementById( 152 | `marker-${objectIndex}-${index}` 153 | ); 154 | if (iconElement) { 155 | iconElement.style.backgroundColor = markerColor; 156 | } 157 | }; 158 | addressElement.onclick = function () { 159 | mymap.setView([lat, lon], 19); 160 | }; 161 | addressList.appendChild(addressElement); 162 | 163 | const finalScore = 164 | (prediction.score - minScore) / (maxScore - minScore); 165 | heatmapData.push([lat, lon, finalScore]); 166 | }); 167 | }); 168 | }); 169 | if (heatLayer) { 170 | mymap.removeLayer(heatLayer); 171 | } 172 | heatLayer = L.heatLayer(heatmapData, { 173 | radius: 43, 174 | blur: 15, 175 | max: 1.0, 176 | gradient: { 0.4: "blue", 0.6: "lime", 1: "red" }, 177 | }).addTo(mymap); 178 | var minScoreTwo = Math.min(...heatmapData.map((item) => item[2])); 179 | var maxScoreTwo = Math.max(...heatmapData.map((item) => item[2])); 180 | function scoreToPercentage(score) { 181 | return ((score - minScoreTwo) / (maxScoreTwo - minScoreTwo)) * 100; 182 | } 183 | function getColor(d) { 184 | return d > 75 ? "red" : d > 50 ? "lime" : d > 25 ? "blue" : "#FFEDA0"; 185 | } 186 | var legend = L.control({ position: "bottomright" }); 187 | legend.onAdd = function (map) { 188 | var div = L.DomUtil.create("div", "info legend"), 189 | grades = [0, 25, 50, 75, 100], 190 | labels = [], 191 | from, 192 | to; 193 | div.id = "legend"; 194 | div.innerHTML = "

Heatmap Legend

"; 195 | div.innerHTML += "

Overall Certainty

"; 196 | for (var i = 0; i < grades.length; i++) { 197 | from = grades[i]; 198 | to = grades[i + 1]; 199 | labels.push( 200 | ' ' + 203 | from + 204 | "%" + 205 | (to ? "–" + to + "%" : "+") 206 | ); 207 | } 208 | div.innerHTML += labels.join("
"); 209 | return div; 210 | }; 211 | if (!document.getElementById("legend")) { 212 | legend.addTo(mymap); 213 | } 214 | } 215 | function sortAndDisplayAddresses(sortType) { 216 | addressElements = [...document.querySelectorAll(".address-item")]; 217 | var addressList = document.getElementById("addressList"); 218 | while (addressList.firstChild) { 219 | addressList.removeChild(addressList.firstChild); 220 | } 221 | if (sortType === "scoreAsc") { 222 | addressElements.sort((a, b) => { 223 | const scoreA = parseFloat(a.dataset.score); 224 | const scoreB = parseFloat(b.dataset.score); 225 | return scoreA < scoreB ? -1 : scoreA > scoreB ? 1 : 0; 226 | }); 227 | } else if (sortType === "nameAsc") { 228 | addressElements.sort((a, b) => { 229 | const nameA = a.dataset.fileName; 230 | const nameB = b.dataset.fileName; 231 | return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; 232 | }); 233 | } 234 | addressElements.forEach((element) => addressList.appendChild(element)); 235 | } 236 | document.getElementById("sortOptions").addEventListener("change", function () { 237 | sortAndDisplayAddresses(this.value); 238 | }); 239 | document.getElementById("loadButton").addEventListener("click", function () { 240 | var fileInput = document.getElementById("fileInput"); 241 | var file = fileInput.files[0]; 242 | if (file) { 243 | var reader = new FileReader(); 244 | reader.onload = function (e) { 245 | var apiData = JSON.parse(e.target.result); 246 | mymap.eachLayer(function (layer) { 247 | if (!layer._url) mymap.removeLayer(layer); 248 | }); 249 | visualizeData(apiData); 250 | }; 251 | reader.readAsText(file); 252 | } else { 253 | alert("Please select a JSON file first."); 254 | } 255 | }); 256 | document.getElementById("searchInput").addEventListener("input", function (e) { 257 | var searchTerm = e.target.value.toLowerCase(); 258 | markers.forEach(function (item) { 259 | var markerContent = item.marker.getTooltip().getContent().toLowerCase(); 260 | if (markerContent.includes(searchTerm)) { 261 | item.marker._icon.style.display = "block"; 262 | } else { 263 | item.marker._icon.style.display = "none"; 264 | } 265 | }); 266 | var addressItems = document.querySelectorAll(".address-item"); 267 | addressItems.forEach(function (item) { 268 | var itemContent = item.innerText.toLowerCase(); 269 | if (itemContent.includes(searchTerm)) { 270 | item.style.display = "block"; 271 | } else { 272 | item.style.display = "none"; 273 | } 274 | }); 275 | }); 276 | function showHistogram(data) { 277 | document.getElementById("mapid").style.display = "none"; 278 | document.getElementById("visualization").style.display = "block"; 279 | document.getElementById("visualization").innerHTML = ""; 280 | document.getElementById("addressList").style.display = "none"; 281 | var visualizationElement = document.getElementById("visualization"); 282 | var visualizationWidth = visualizationElement.offsetWidth; 283 | var visualizationHeight = 500; 284 | var scores = []; 285 | data.forEach(function (imageData) { 286 | Object.values(imageData).forEach(function (details) { 287 | details.geo_predictions.forEach(function (prediction) { 288 | scores.push(prediction.score); 289 | }); 290 | }); 291 | }); 292 | var svg = d3 293 | .select("#visualization") 294 | .append("svg") 295 | .attr("width", visualizationWidth) 296 | .attr("height", visualizationHeight); 297 | var margin = { top: 10, right: 30, bottom: 50, left: 80 }, 298 | width = +svg.attr("width") - margin.left - margin.right, 299 | height = +svg.attr("height") - margin.top - margin.bottom; 300 | var g = svg 301 | .append("g") 302 | .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 303 | var maxScore = d3.max(scores); 304 | var x = d3 305 | .scaleLinear() 306 | .domain([0, maxScore + 0.005]) 307 | .range([0, width]); 308 | var bins = d3.histogram().domain(x.domain()).thresholds(x.ticks(20))(scores); 309 | var y = d3 310 | .scaleLinear() 311 | .domain([ 312 | 0, 313 | d3.max(bins, function (d) { 314 | return d.length; 315 | }), 316 | ]) 317 | .range([height, 0]); 318 | var colorScale = d3.scaleSequential(d3.interpolateBlues).domain([ 319 | 0, 320 | d3.max(bins, function (d) { 321 | return d.length; 322 | }), 323 | ]); 324 | var bar = g 325 | .selectAll(".bar") 326 | .data(bins) 327 | .enter() 328 | .append("g") 329 | .attr("class", "bar") 330 | .attr("transform", function (d) { 331 | return "translate(" + x(d.x0) + "," + y(d.length) + ")"; 332 | }); 333 | bar 334 | .append("rect") 335 | .attr("x", 1) 336 | .attr("width", x(bins[0].x1) - x(bins[0].x0) - 1) 337 | .attr("height", function (d) { 338 | return height - y(d.length); 339 | }) 340 | .attr("fill", function (d) { 341 | return colorScale(d.length); 342 | }) 343 | .on("mouseover", function (event, d) { 344 | d3.select(this).attr("fill", "orange"); 345 | tooltip.style("visibility", "visible").text("Count: " + d.length); 346 | }) 347 | .on("mousemove", function (event) { 348 | tooltip 349 | .style("top", event.pageY - 10 + "px") 350 | .style("left", event.pageX + 10 + "px"); 351 | }) 352 | .on("mouseout", function (event, d) { 353 | d3.select(this).attr("fill", colorScale(d.length)); 354 | tooltip.style("visibility", "hidden"); 355 | }); 356 | bar 357 | .append("text") 358 | .attr("dy", ".75em") 359 | .attr("y", -12) 360 | .attr("x", (x(bins[0].x1) - x(bins[0].x0)) / 2) 361 | .attr("text-anchor", "middle") 362 | .style("fill", "#fff") 363 | .style("font-size", "12px") 364 | .text(function (d) { 365 | return d.length; 366 | }); 367 | var xAxis = g 368 | .append("g") 369 | .attr("class", "axis axis--x") 370 | .attr("transform", "translate(0," + height + ")") 371 | .call(d3.axisBottom(x).tickSizeOuter(0)); 372 | xAxis 373 | .append("text") 374 | .attr("fill", "#000") 375 | .attr("x", width / 2) 376 | .attr("y", 40) 377 | .attr("text-anchor", "middle") 378 | .style("font-size", "16px") 379 | .style("font-weight", "bold") 380 | .text("Confidence Scores"); 381 | var yAxis = g 382 | .append("g") 383 | .attr("class", "axis axis--y") 384 | .call(d3.axisLeft(y).tickSizeOuter(0)); 385 | yAxis 386 | .append("text") 387 | .attr("fill", "#000") 388 | .attr("transform", "rotate(-90)") 389 | .attr("x", -height / 2) 390 | .attr("y", -50) 391 | .attr("text-anchor", "middle") 392 | .style("font-size", "16px") 393 | .style("font-weight", "bold") 394 | .text("Frequency"); 395 | g.append("g") 396 | .attr("class", "grid") 397 | .call(d3.axisLeft(y).tickSize(-width).tickFormat("")) 398 | .attr("opacity", 0.3) 399 | .selectAll("line") 400 | .attr("stroke", "darkgray") 401 | .attr("stroke-dasharray", "2,2"); 402 | g.append("g") 403 | .attr("class", "grid") 404 | .attr("transform", "translate(0," + height + ")") 405 | .call(d3.axisBottom(x).tickSize(-height).tickFormat("")) 406 | .attr("opacity", 0.3) 407 | .selectAll("line") 408 | .attr("stroke", "darkgray") 409 | .attr("stroke-dasharray", "2,2"); 410 | var tooltip = d3.select(".tooltip"); 411 | if (tooltip.empty()) { 412 | tooltip = d3 413 | .select("body") 414 | .append("div") 415 | .attr("class", "tooltip") 416 | .style("position", "absolute") 417 | .style("visibility", "hidden") 418 | .style("padding", "10px") 419 | .style("background", "rgba(0, 0, 0, 0.75)") 420 | .style("border-radius", "5px") 421 | .style("color", "#fff") 422 | .style("font-size", "14px") 423 | .style("text-align", "center"); 424 | } 425 | var zoom = d3 426 | .zoom() 427 | .scaleExtent([1, 5]) 428 | .translateExtent([ 429 | [-margin.left, -margin.top], 430 | [width + margin.right, height + margin.bottom], 431 | ]) 432 | .on("zoom", zoomed); 433 | svg.call(zoom); 434 | function zoomed(event) { 435 | g.attr("transform", event.transform); 436 | xAxis.call(d3.axisBottom(x).scale(event.transform.rescaleX(x))); 437 | yAxis.call(d3.axisLeft(y).scale(event.transform.rescaleY(y))); 438 | } 439 | } 440 | function showMap() { 441 | document.getElementById("mapid").style.display = "block"; 442 | document.getElementById("visualization").style.display = "none"; 443 | document.getElementById("addressList").style.display = "block"; 444 | document.getElementById("sortOptions").disabled = false; 445 | document.getElementById("searchInput").disabled = false; 446 | } 447 | document.getElementById("showMapButton").addEventListener("click", function () { 448 | showMap(); 449 | }); 450 | document 451 | .getElementById("showHistogramButton") 452 | .addEventListener("click", function () { 453 | showHistogram(initialData); 454 | document.getElementById("sortOptions").disabled = true; 455 | document.getElementById("searchInput").disabled = true; 456 | }); 457 | showMap(); 458 | --------------------------------------------------------------------------------