Aviation safety data collected by the National Transportation Safety Board (NTSB) spans over six decades of US civil aviation accidents. The database contains detailed records including aircraft information, injury severity, weather conditions, probable cause narratives, and geographic coordinates for each event. While this data is publicly available, exploring it in its raw form offers little insight without the ability to filter, compare, and visualize patterns across multiple dimensions simultaneously. This project builds an interactive, browser-based visualization platform that enables real-time exploration of 168,793 NTSB aviation accident records from 1962 to 2025, combining geographic mapping with analytical charts and multi-dimensional filtering to surface patterns in the data that static reports cannot reveal.
The NTSB Aviation Accident Database is one of the most comprehensive aviation safety datasets in the world, yet accessing and analyzing it remains difficult for most users. The raw data is delivered as flat CSV files with inconsistent formatting across decades of data entry by different investigators. Many records lack precise coordinates, aircraft field naming conventions changed over the years, and multi-aircraft events are stored as comma-separated values within single fields rather than normalized across rows. Building a visualization tool on this data required not only the technical implementation of interactive maps and charts but also significant data cleaning and enrichment to make the records usable. The challenge was to build a single-page application capable of rendering 168,793 records across multiple visualization modes with real-time filtering, without requiring any server-side infrastructure.
The project was built as a single HTML file with all functionality implemented client-side using JavaScript. The technology stack was selected based on the strengths of each library for specific visualization tasks:
All basemap tiles are served from external tile providers. CARTO provides the dark and street map tiles, the FAA provides VFR sectional chart tiles, and OpenTopoMap provides terrain/elevation tiles. The application requires no backend server and runs entirely in the browser, making it suitable for deployment on GitHub Pages as a static site.
The raw NTSB dataset required significant cleaning before it could be used effectively in the visualization. The following preprocessing steps were performed offline using Python 3 with Pandas for data manipulation, CSV parsing, and analysis, and Shapely for geographic point-in-polygon computations:
Coordinate Recovery: A substantial number of older accident records lacked precise GPS coordinates. Using airport identifier codes and city/state information from the records, coordinates were recovered by cross-referencing against a database of 32,452 US airports and a city centroid lookup table. Each record's coordinate source was tagged (GPS/survey, airport lookup, city centroid, or state centroid) to maintain transparency about data quality. Records with city-level coordinates were flagged in the application's popup display with a warning indicator.
Coordinate Jitter: Many accidents geocoded to city centroids shared identical coordinates, causing dots to stack on top of each other on the map. A random offset of approximately 1km was applied to city and state centroid records so individual accidents spread apart when zoomed in while still appearing clustered at the city level when zoomed out.
Engine Count Correction: The NumberOfEngines field contained numerous blanks and errors. Gliders and balloons with blank engine counts were set to 0. Helicopters and airplanes with missing counts were identified by Make and Model and assigned the correct engine count. Known single-engine models (Bell 206, Cessna 172, etc.) were set to 1, and known twin-engine models (Sikorsky S-76, Bell 412, Boeing 737, etc.) were set to 2 or 4 as appropriate. Individual records were verified against publicly available aircraft specifications.
Multi-Aircraft Flagging: Multi-aircraft accidents (midair collisions, runway incursions, ground collisions) were identified using two methods. First, records with comma-separated values in the AirCraftCategory field indicated two aircraft were involved in the same event. Second, the ProbableCause text was searched for keywords including "midair collision," "other aircraft," "runway incursion," and similar phrases. Both methods were combined and the results were manually reviewed to remove false positives such as bird strikes, tree collisions, and other single-aircraft events that happened to mention another aircraft in the narrative. A total of 2,124 multi-aircraft accidents were identified and flagged.
County Assignment: Each accident record was assigned to a US county using point-in-polygon geometric testing against 3,220 county boundary polygons. A bounding box pre-filter was applied to reduce the computational cost by eliminating county polygons that could not possibly contain a given point before running the full geometry check. County FIPS codes and names were stored directly in the CSV to eliminate any runtime geometry calculations. The Python Shapely library with prepared geometries provided the spatial analysis.
State Centroid Records: Approximately 14,658 records had coordinates placed at their state's geographic centroid due to insufficient location data in the original NTSB records. These records were merged into the main dataset with all enrichments applied but are identified separately in the application. Their data contributes to all statistical calculations but their map positions are acknowledged as approximate through "unlocated" indicators in the spike map and county drill-down views.
The application provides four basemap options and three interactive map modes for viewing accident data on the Leaflet map:
Cluster Mode groups nearby accident markers into numbered circles that split apart as the user zooms in. Cluster icons are color-coded by the worst injury severity in the group (red for fatal, orange for serious, yellow for minor, green for none). Individual markers use DOM-based divIcon elements to ensure reliable click interaction for opening accident detail popups. A batched rendering system with a progress indicator prevents the browser from freezing during the creation of 168,793 markers, and mode-switching buttons are disabled during rendering to prevent users from interrupting the process.
Heatmap Mode renders accident density as a continuous color gradient using the Leaflet.heat plugin. Intensity is weighted by injury severity so fatal accidents contribute more visual weight than minor incidents.
All Dots Mode renders every accident as an individual circle marker on an HTML5 canvas element for performance. A batched rendering system with a token-based cancellation mechanism prevents old render batches from contaminating new renders when filters change mid-build. Dot size scales with zoom level for visibility at different scales.
Timeline Mode animates through the dataset year by year, re-rendering the current map mode for each year's data with a playback transport bar showing year, progress, and play/pause controls.
A full-screen D3.js spike map provides a state-level overview where vertical spikes rise from each state proportional to accident count. Spike color transitions from blue (low) through cyan (mid) to red (high). Hovering over any state or spike displays a tooltip with accident count, fatal count, fatality rate, total fatalities, and the number of unlocated records for that state.
Clicking a state transitions to a county-level drill-down view. County boundaries are rendered as a choropleth colored by accident density using a threshold scale with six color buckets. Individual accident dots are rendered on an HTML5 canvas element overlaid on the SVG county paths. This separation ensures that the canvas dots do not interfere with SVG hover event detection on the county polygons, providing instant tooltip response regardless of how many dots are rendered. A clip path derived from the merged state boundary prevents dots from rendering outside the state outline. A back button returns to the national spike map view.
County accident counts are derived from the pre-computed CountyFIPS field in the CSV, requiring zero runtime geometry. When a state has unlocated records (state-centroid coordinates), county hover counts are prefixed with a tilde (~) to indicate the values are approximate minimums.
Eight filter dimensions are available, all operating simultaneously with AND logic between groups and OR logic within groups:
All filters update every visualization in real time including the map, charts, sparklines, statistics, choropleth, spike map, and county drill-down. Chip filters support single-click toggle and double-click isolate (deactivates all others in the group).
A slide-out analytics drawer contains four D3.js visualizations at full size, all updating in real time with the current filtered dataset:
The sidebar header contains four sparkline SVG charts showing year-over-year trends for total accidents, fatal accidents, serious injuries, and fatalities. These update with every filter change providing an immediate visual indicator of whether a trend is improving or worsening for the selected filter combination.
Small versions of the year chart and makes chart are also rendered in the sidebar scroll area for quick reference without opening the analytics drawer.
The layout adapts across screen sizes using CSS media queries at four breakpoints. The sidebar narrows from 340px to 260px on smaller screens. The analytics and help drawers scale their width using calc() based on viewport width. On mobile devices the drawers render as full-screen overlays above the sidebar. All D3 charts use their container's clientWidth for responsive sizing.
NTSB Data Inconsistency: The most significant challenge was the inconsistency of the source data across six decades. Aircraft make names appeared in multiple formats (CESSNA, Cessna, cessna). The NumberOfEngines field contained blanks, zeros, and comma-separated values for multi-aircraft events. Probable cause narratives used varied phrasing for the same causal factors. Builder names with commas in the Make field (e.g., "ANKERMAN, DONALD L.") were initially misidentified as multi-aircraft records. Each inconsistency required detection, analysis, and a targeted fix.
Canvas vs DOM Rendering: Leaflet's preferCanvas option caused cluster markers rendered as circleMarkers on a canvas element to lose click interactivity. Switching cluster markers to DOM-based divIcon elements resolved the click issue but increased rendering time due to creating 168,793 individual DOM elements. The All Dots mode uses a dedicated canvas renderer that must be properly destroyed (not just hidden) when switching modes to prevent the canvas from intercepting click events on other layers.
Cluster Rendering Performance: Building 168,793 MarkerCluster markers takes several seconds even on fast hardware. A batched rendering system with a progress bar and disabled mode buttons was implemented to prevent users from clicking away during the build process. A requestAnimationFrame polling mechanism detects when cluster icons actually appear in the DOM before dismissing the loading indicator, ensuring consistent behavior regardless of machine speed. An edge case where zero filtered records caused the polling to run indefinitely was addressed with an early exit check.
County Drill-Down Performance: Initial implementations performed point-in-polygon geometry checks at runtime for each accident against all county polygons when a state was clicked. This took 4+ seconds for large states like Texas and California. Moving the county assignment to offline preprocessing and storing the results in the CSV eliminated all runtime geometry, making county views render instantly.
County Hover Lag: Rendering thousands of SVG circle elements for accident dots caused severe hover lag on county polygons due to browser hit-testing through the DOM tree. Replacing SVG circles with a single HTML5 canvas element for dot rendering eliminated the lag entirely while maintaining SVG-based interactive county hover tooltips.
The project demonstrates that a single-page browser application with no backend infrastructure can effectively visualize and enable interactive exploration of a large-scale aviation safety dataset. The combination of Leaflet for geographic visualization and D3.js for analytical charts provides complementary strengths. Leaflet handles the interactive pan/zoom map with efficient tile rendering and marker management. D3 provides the precise control needed for custom chart types including the spike map, choropleth, and donut chart.
The data preprocessing pipeline proved to be as significant as the visualization implementation itself. The raw NTSB data required coordinate recovery, standardization of inconsistent fields, flagging of multi-aircraft events, and geographic enrichment with county-level assignments. Without this preprocessing, the visualization would have been limited to plotting raw coordinates with no meaningful filtering or geographic aggregation.
The spike map with county drill-down provides a visualization capability not available in the NTSB's own query tools. Users can identify state-level patterns, drill into county-level density, and cross-reference with filters for aircraft type, weather conditions, and injury severity to investigate specific scenarios.
Future work on this project could include automated classification of probable cause narratives using natural language processing or fine-tuned language models. A pre-computed cause category field would enable real-time analysis of why accidents happen for any filtered combination, transitioning the tool from a visualization platform to an analytical one. Integration of a small fine-tuned language model running locally via WebLLM could enable conversational queries against the dataset directly within the application.
The project integrates geographic visualization, analytical charting, and multi-dimensional filtering into a single interactive platform for exploring NTSB aviation accident data. Six D3.js visualization types (line chart, bar chart, donut chart, sparklines, choropleth, and spike map) work alongside three Leaflet map modes (cluster, heatmap, all dots) and a timeline animation to provide multiple perspectives on the same dataset. All visualizations update in real time as filters are applied. The spike map with state-to-county drill-down enables geographic analysis from national overview to individual county level. The entire application runs client-side as a static site deployable on GitHub Pages with no server dependencies.
From choosing a visualization approach, cleaning and enriching 168,793 records of inconsistent government data, implementing multiple rendering strategies to handle performance constraints, and building an interactive analytical tool from start to finish as an individual teaches the integration of multiple JavaScript libraries, the challenges of working with real-world messy data, and the importance of preprocessing in any data visualization pipeline.
This addendum presents the rationale for selecting Leaflet over deck.gl as the mapping library for an interactive visualization of approximately 170,000 NTSB aviation accident records (1962–present), deployed as a static site via GitHub Pages.
deck.gl was evaluated due to its GPU-accelerated rendering capabilities for large-scale geospatial data. However, there was no deck.gl requirement listed in the syllabus for the final project so Leaflet was selected based on: equivalent basemap rendering for raster tile services, broader client hardware accessibility, compatibility with static hosting, and sufficient performance for the project's requirements.
This was a deliberate architectural decision, not a capability limitation.
Leaflet: a lightweight JavaScript mapping library that renders via DOM and HTML5 Canvas (CPU-bound). Requires no build pipeline, has an extensive plugin ecosystem including marker clustering, and supports Canvas-accelerated rendering.
deck.gl: WebGL-powered framework for large-scale data visualization. Renders directly on the GPU, enabling smooth interaction with million-point datasets. Typically requires a JavaScript bundler and is commonly paired with Mapbox GL or MapLibre for basemaps.
The visualization uses Carto's Voyager and Dark Matter basemaps, accessed as raster tile endpoints. Both libraries consume these identically—Leaflet via L.tileLayer(), deck.gl via TileLayer. The tiles are pre-rendered PNGs fetched from Carto's servers. Neither library provides a rendering advantage for raster basemaps.
The same applies to FAA sectional chart overlays. These are raster products served as XYZ tiles. Both libraries simply drape these images over the map canvas; neither interprets or renders them from vector data.
A deck.gl + MapLibre implementation could render vector tiles client-side, enabling runtime style customization. This capability would have added complexity without benefit.
deck.gl's WebGL pipeline assumes GPU availability. The following configurations may experience degraded performance or failure: older integrated graphics (Intel HD, AMD APUs), mobile devices with limited GPU memory, Chromebooks with restricted WebGL contexts, virtual machines without GPU passthrough, and corporate environments with WebGL disabled by policy.
| Condition | Leaflet Behavior | deck.gl Behavior |
|---|---|---|
| Slow CPU | Sluggish panning, delayed rendering | Minimal impact |
| Weak/no GPU | No impact | Severe lag, crashes, blank canvas |
| WebGL disabled | No impact | Complete failure |
| Low memory | Gradual slowdown | WebGL context loss, crash |
Leaflet degrades gracefully — users experience slower performance but retain functionality. deck.gl fails catastrophically — when GPU resources are unavailable, rendering the visualization unusable or completely blank.
The target audience includes aviation safety researchers, students, pilots, and general users who cannot be assumed to have high-performance hardware. Leaflet ensures functionality across the widest range of client configurations, consistent with accessible design principles.
The project was deployed via GitHub Pages as a static site—HTML, CSS, and JavaScript served without server-side processing or build steps.
Leaflet operates natively in this environment via CDN script include. deck.gl typically requires npm integration and a bundler (Webpack, Vite) to produce browser-compatible output. While deck.gl can load from CDN, this sacrifices tree-shaking, inflates bundle size, and complicates dependency management.
| Aspect | Leaflet | deck.gl |
|---|---|---|
| CDN usage | Native | Suboptimal |
| Bundler required | No | Typically yes |
| Setup time | Minutes | Hours |
| Dataset Size | Leaflet (Canvas/Cluster) | deck.gl (WebGL) |
|---|---|---|
| 10,000 points | Smooth | Smooth |
| 50,000 points | Acceptable | Smooth |
| 170,000 points | Acceptable with clustering | Smooth |
| 1,000,000+ points | Degraded | Smooth |
For 170,000 static records with marker clustering, Leaflet provided acceptable performance. The visualization does not require real-time animation, streaming data, or GPU-accelerated aggregation—scenarios where deck.gl's overhead yields significant return.
These tradeoffs were acceptable given project scope and audience.
Leaflet was selected over deck.gl based on:
deck.gl is appropriate for million-point datasets, real-time streaming, or GPU-accelerated aggregation. For this project's requirements, Leaflet provided equivalent functionality with broader accessibility and lower implementation overhead.