A full-stack data pipeline and API that power a public Gun Violence Dashboard for the City of Philadelphia. Visit the live map: https://nickhand.dev/philly-gun-violence-map
- Vue 3 dashboard with interactive maps and data visualizations.
- FastAPI service for geospatial endpoints and dashboard data.
- ETL pipelines that ingest, clean, and enrich shootings, homicides, courts, and boundaries data.
- Shared utilities for AWS/S3 access and shared data models.
- Automation via GitHub Actions, Fly.io, and Netlify deployment.
- End-to-end geospatial data platform powering a public dashboard used by civic audiences.
- Vue 3 frontend with MapLibre GL maps, D3.js charts, and Vuetify components.
- Automated ETL pipelines with scheduled refreshes, validation, and S3-backed storage.
- FastAPI service optimized for large GeoJSON payloads with pagination and caching.
- Shared, typed data models across ETL and API for consistent contracts.
- Production deployment on Fly.io (API) and Netlify (frontend) with CI/CD automation.
This application was originally developed by Nick Hand for the Philadelphia City Controller’s Office and later migrated to his personal website as a rebuilt, improved version.
It relies only on public data sources:
- Shooting victims: City of Philadelphia open data (OpenDataPhilly.org)
- Homicide totals: Philadelphia Police Department crime statistics site
- Court cases: Pennsylvania Unified Judicial System web portal
- Shooting victims data is updated daily on OpenDataPhilly (typically by ~10:30am on weekdays).
- Homicide totals include all homicide types, not just firearm-related incidents.
- All data is preliminary and may differ from other public incident datasets.
- Court case matches are derived by searching the DC number in the Philadelphia Municipal Court portal; updates run weekly.
Prereqs: Python 3.13, uv, just, AWS CLI, Fly CLI (for deploys).
- Create
.envfrom.env.exampleand set AWS credentials and bucket. - Run the API locally:
just api-dev- Run ETL jobs (examples):
just etl-shootings
just etl-homicides
just etl-courts
just etl-streets- Pull down S3 data locally (optional):
just data-sync- ETL jobs write processed data to S3 (
processed/*.geojson,processed/*_meta.json) - API loads data from S3 at startup, indexes by year, and caches in memory
- Frontend fetches metadata, then loads year-specific NDJSON data on demand
- GitHub Actions trigger ETL + Fly restart on schedules for freshness
The shootings endpoint uses a versioned, content-addressed caching strategy:
GET /shootings/meta— Returns version hash, available years, and per-year URLsGET /shootings/rows/{version}/{year}.ndjson— Year-specific data (immutable, cached 1 year)
The frontend builds GeoJSON client-side from the NDJSON rows, avoiding duplicate data transfer.
api/ FastAPI service
etl/ ETL pipelines and CLI
dashboard-utils/ Shared AWS + data utilities + models
frontend/ Vue 3 frontend application
The dashboard UI is a Vue 3 single-page application with interactive maps and charts.
Tech stack:
- Vue 3 with Composition API
- Vuetify 3 for Material Design components
- MapLibre GL for interactive mapping
- D3.js for data visualizations and charts
- Arquero for in-browser data filtering
- Pinia for state management
- Vite for build tooling
Project structure:
frontend/src/
├── app/ App shell and layout
├── features/ Feature modules (map, charts, filters)
├── pages/ Route-level page components
├── shared/ Shared utilities, API client, stores
├── types/ TypeScript type definitions
└── main.ts Application entry point
Development:
cd frontend
npm install
npm run dev # Start dev server at http://localhost:5173
npm run build # Production build to dist/Deployment:
The frontend is deployed to Netlify. Pushes to main trigger automatic builds.
fly.tomldefines app config.api/Dockerfilebuilds the API image.
just fly-secrets-api
just fly-deploy-apiIf this project is useful or you want to collaborate, check out: https://nickhand.dev
Please open an issue: https://github.com/nickhand/philly-gun-violence-dashboard/issues
