Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"permissions": {
"allow": [
"Bash(codex exec \"continue to next task\" --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox)",
"Bash(powershell.exe:*)",
"Bash(dir:*)",
"Bash(if not exist logs mkdir logs)",
"Bash(test:*)",
"Bash(node:*)",
"Bash(cat:*)",
"Bash(find:*)",
"Bash(timeout 10 npm run dev:*)",
"Bash(Start-Sleep -Seconds 8)",
"Bash($ts = Get-Date -Format \"yyyyMMdd_HHmmss\")",
"Bash(Invoke-WebRequest -Uri \"http://localhost:5173/\" -OutFile \"logs/diag_compare_http_$ts.log\" -TimeoutSec 5)",
"Bash(Out-String)",
"Bash(cmd /c \"dir logs\\*.log /O-D /B\")",
"Bash(awk:*)",
"Bash(echo $ts)",
"Bash(npm run build:*)",
"Bash(curl -v http://localhost:4173/)",
"Bash(Write-Output $ts)",
"Bash(curl -s \"https://phl.carto.com/api/v2/sql?q=SELECT%20dc_dist%2C%20COUNT(*)%20AS%20n%20FROM%20incidents_part1_part2%20WHERE%20dispatch_date_time%20%3E%3D%20%272022-01-01%27%20AND%20dispatch_date_time%20%3C%20%272022-06-01%27%20GROUP%20BY%201%20ORDER%20BY%201%20LIMIT%203&format=json\")",
"Bash(curl -s \"https://phl.carto.com/api/v2/sql?q=SELECT%20dc_dist%2C%20COUNT(*)%20AS%20n%20FROM%20incidents_part1_part2%20WHERE%20dispatch_date_time%20%3E%3D%20%272022-01-01%27%20AND%20dispatch_date_time%20%3C%20%272022-06-01%27%20AND%20text_general_code%20IN%20(%27Thefts%27)%20GROUP%20BY%201%20ORDER%20BY%201%20LIMIT%203&format=json\")",
"Bash(curl -s \"https://phl.carto.com/api/v2/sql?q=SELECT%20dc_dist%2C%20COUNT(*)%20AS%20n%20FROM%20incidents_part1_part2%20WHERE%20dispatch_date_time%20%3E%3D%20%272022-01-01%27%20AND%20dispatch_date_time%20%3C%20%272022-06-01%27%20AND%20text_general_code%20IN%20(%27Motor%20Vehicle%20Theft%27%2C%27Theft%20from%20Vehicle%27)%20GROUP%20BY%201%20ORDER%20BY%201%20LIMIT%203&format=json\")",
"Bash(curl -s \"https://mapservices.pasda.psu.edu/server/rest/services/pasda/CityPhilly/MapServer/28/query?where=1%3D1&outFields=*&f=geojson\")",
"Bash(curl -s \"https://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/Tracts_Blocks/MapServer/0/query?where=STATE%3D%2742%27%20AND%20COUNTY%3D%27101%27&outFields=STATE,COUNTY,GEOID,NAME,BASENAME,ALAND,AWATER&f=geojson\")",
"Bash(curl -s \"https://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/Tracts_Blocks/MapServer/0/query?where=STATE=''''42''''%20AND%20COUNTY=''''101''''&outFields=STATE,COUNTY,GEOID,NAME,BASENAME&returnGeometry=true&f=geojson\")",
"Bash(python -m json.tool)",
"Bash(curl -s -I http://localhost:4173/)",
"Bash(npm run preview)",
"Bash(if test -f \"public/data/tracts_phl.geojson\")",
"Bash(then ls -lh \"public/data/tracts_phl.geojson\")",
"Bash(fi)",
"Bash(tree -L 3 -d -I 'node_modules' .)",
"Bash(git rm -r --cached dist/)",
"Bash(git grep:*)",
"Bash(npm run dev:*)",
"Bash(python3:*)",
"Bash(curl:*)",
"Bash(if test -f \"src/data/tract_crime_counts_last12m.json\")",
"Bash(then ls -lh \"src/data/tract_crime_counts_last12m.json\")",
"Bash(else echo \"tract_crime_counts_last12m.json not found\")",
"Bash(if test:*)",
"Bash(then ls -lh \"src/data/tract_counts_last12m.json\")",
"Bash(else echo \"tract_counts_last12m.json not found\")"
],
"deny": [],
"ask": []
}
}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
node_modules/
dist/
logs/
.DS_Store
*.local
.env*
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"chatgpt.openOnStartup": true
}
147 changes: 146 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,146 @@
Add a readme for your dashboard here. Include content overview, data citations, and any relevant technical details.
# Philadelphia Crime Dashboard

An interactive web dashboard for exploring crime incidents in Philadelphia using MapLibre GL, Chart.js, and the City of Philadelphia's open data APIs.

## How to Use the Dashboard

### Setting Your Area of Interest (Buffer A)
1. Click **"Select on Map"** button to enter selection mode
2. Click anywhere on the map to set your buffer center (marker A will appear)
3. Choose a **radius** (400m, 800m, 1.6km, or 3.2km) to define your area
4. An orange circle shows your selected buffer zone

### Time Window Controls
- **Quick Presets:** Click "Last 3mo", "Last 6mo", or "Last 12mo" for recent data
- **Custom Range:** Use the start month picker + duration dropdown to query historical windows (e.g., Jan 2023 - Jun 2023)

### Filtering by Crime Type
- **Offense Groups:** Select broad categories (Property, Violent, Vehicle, etc.) from the multi-select
- **Drilldown:** After selecting groups, the fine-grained codes dropdown populates with specific offense types (e.g., "THEFT", "RETAIL THEFT")
- Choose specific codes to narrow your analysis further

### Map Layers & Visualization
- **Admin Level Toggle:** Switch between **Police Districts** and **Census Tracts** views
- **Display Mode:** Toggle between raw **counts** and **per-10k population** rates
- **Click districts/tracts** for detailed popup stats (total incidents, per-10k rate, 30-day trends, top-3 offense types)
- **Hover** over any polygon to see quick stats in the tooltip

### Charts & Compare Card
- **Monthly Series:** Line chart comparing your buffer (A) vs citywide trends
- **Top Offenses:** Bar chart showing most frequent crime types in buffer A
- **7x24 Heatmap:** Hour-of-day and day-of-week patterns
- **Compare A Card:** Live summary with total incidents, per-10k rate, 30-day change, and top-3 offenses

For detailed control semantics and technical specifications, see [docs/CONTROL_SPEC.md](docs/CONTROL_SPEC.md).

## Quick Start (Dev vs Preview)

> **⚠️ CRITICAL:** Do NOT open `index.html` directly in your browser. The app requires a bundler (Vite) to resolve ES modules and dependencies.

> **📁 Vite Project Structure Rule:** In Vite projects, `index.html` MUST be in the **project root**, not in `public/`. The `public/` directory is for static assets (images, fonts) copied as-is. If you see build errors about "HTML proxy", verify `index.html` is at project root with script tags using absolute paths like `/src/main.js`.

### Development Mode (Recommended)
```bash
npm install
npm run dev
```
- Opens at `http://localhost:5173/`
- Hot module replacement (instant updates on file save)
- Full dev tools and error reporting

### Production Preview
```bash
npm run build
npm run preview
```
- Builds optimized bundle to `dist/`
- Serves production build at `http://localhost:4173/`
- Use this to test before deploying

**Current Status (2025-10-15):** Build currently fails due to `vite.config.js` `root: 'public'` configuration. See [docs/KNOWN_ISSUES.md](docs/KNOWN_ISSUES.md) for active blockers and [docs/DEPLOY.md](docs/DEPLOY.md) for detailed troubleshooting.

### Basemap & CSS

- The base map uses OpenStreetMap raster tiles: `https://tile.openstreetmap.org/{z}/{x}/{y}.png`.
- The MapLibre GL CSS is linked via unpkg in `public/index.html` to keep setup simple:
`<link href="https://unpkg.com/maplibre-gl@^4.5.0/dist/maplibre-gl.css" rel="stylesheet" />`
- Attribution: © OpenStreetMap contributors.

### Charts

## How to Use the Dashboard

- Use map selection: click “Select on map�?then click the map to set the A center; press Esc or click the button again to cancel. A translucent circle shows the current buffer.
- Radius: changes the buffer radius used by points, charts, and the compare card; the district choropleth is unaffected by radius.
- Time window: pick a start month and duration (3/6/12/24). Presets “Last 6m/12m�?help jump quickly.
- Offense grouping & drilldown: pick one or more groups, then optionally drill down into specific codes (the list reflects live audited codes).
- Admin level: switch between Districts and Tracts; per�?0k requires tracts + ACS.
- Clusters: when too many points are present, clusters are shown with a prompt to zoom in.

- Charts are implemented with Chart.js v4. Before running the app, install dependencies:
`npm i`
- First run may download ~1�? MB of packages.
- Rebuild anytime with `npm run build`.
- Requires `npm i` to install chart.js; see `logs/vite_build_*.log` for bundling status.

## Data Sources

- CARTO SQL API (City of Philadelphia): https://phl.carto.com/api/v2/sql
- Police Districts (GeoJSON):
https://policegis.phila.gov/arcgis/rest/services/POLICE/Boundaries/MapServer/1/query?where=1=1&outFields=*&f=geojson
- Census Tracts (Philadelphia subset, GeoJSON):
https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/USA_Census_Tracts/FeatureServer/0/query?where=STATE_FIPS='42'%20AND%20COUNTY_FIPS='101'&outFields=FIPS,STATE_FIPS,COUNTY_FIPS,TRACT_FIPS,POPULATION_2020&f=geojson
- ACS 2023 5‑Year (population/tenure/income):
https://api.census.gov/data/2023/acs/acs5?get=NAME,B01003_001E,B25003_001E,B25003_003E,B19013_001E&for=tract:*&in=state:42%20county:101
- ACS 2023 5‑Year Subject (poverty rate):
https://api.census.gov/data/2023/acs/acs5/subject?get=NAME,S1701_C03_001E&for=tract:*&in=state:42%20county:101

## Limitations

- UCR categories are generalized for reporting and do not reflect full incident coding.
- Incident locations are rounded to the hundred block; exact addresses are not provided.
- Counts in this tool may differ from official UCR reports due to methodology and updates.

## Caching & Boundaries

- Police Districts are cached at `public/data/police_districts.geojson` when available.
- At runtime, the app loads the cached file first; if not present or invalid, it falls back to the live ArcGIS service above.

## Performance Policies

- Never fetch the full incidents table; all requests are constrained by a time window and, when the map is visible, the current map bounding box.
- If a points query returns more than 20,000 features, the app hides individual points and prompts the user to zoom, showing clusters instead.
- Clustering is enabled for point sources to improve rendering performance and legibility.

## Compare A/B Semantics

- “A vs B�?compares buffer‑based totals around two centers using the same time window and offense filters.
- Per�?0k rates are only computed when the Tracts layer and ACS population are loaded for the relevant geography; otherwise per�?0k is omitted.

## Tracts + ACS (per�?0k)

- The "Tracts" admin level uses cached tracts geometry and ACS 2023 tract stats.
- Per�?0k rates are computed as (value / population) * 10,000 when population data is available.
- Tracts with population < 500 are masked from the choropleth to avoid unstable rates.

## Precompute tract counts

- To speed up tracts choropleths for longer windows, you can precompute last�?2‑months crime counts per tract:
- Run: `node scripts/precompute_tract_counts.mjs`
- Output JSON: `src/data/tract_counts_last12m.json`
- Logs: `logs/precompute_tract_counts_*.log`
- Data freshness: re‑run the script periodically to refresh counts. The app will use the precomputed file when present, and fall back to live computations otherwise.

## Technical Documentation

- **Control Specifications:** [docs/CONTROL_SPEC.md](docs/CONTROL_SPEC.md) - Detailed state model, event flows, visual aids, and edge cases for all UI controls
- **Fix Plan:** [docs/FIX_PLAN.md](docs/FIX_PLAN.md) - Root cause analysis and implementation steps for known UX/logic issues
- **TODO:** [docs/TODO.md](docs/TODO.md) - Actionable task list with acceptance tests
- **Deployment Guide:** [docs/DEPLOY.md](docs/DEPLOY.md) - Run modes (dev/preview), why raw file access fails, troubleshooting
- **Known Issues:** [docs/KNOWN_ISSUES.md](docs/KNOWN_ISSUES.md) - Current blockers, performance issues, workarounds
- **Changelog:** [docs/CHANGELOG.md](docs/CHANGELOG.md) - Feature history, implementation notes, diagnostic logs


Quick Start: index.html is at repo root; use
pm run dev for local dev or
pm run preview after a build.
106 changes: 106 additions & 0 deletions TEMP_main_line.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import './style.css';
import dayjs from 'dayjs';
import { initMap } from './map/initMap.js';
import { getDistrictsMerged } from './map/choropleth_districts.js';
import { renderDistrictChoropleth } from './map/render_choropleth.js';
import { drawLegend } from './map/ui_legend.js';
import { attachHover } from './map/ui_tooltip.js';
import { wirePoints } from './map/wire_points.js';
import { updateAllCharts } from './charts/index.js';
import { store } from './state/store.js';
import { initPanel } from './ui/panel.js';
import { refreshPoints } from './map/points.js';
import { updateCompare } from './compare/card.js';

window.__dashboard = {
setChoropleth: (/* future hook */) => {},
};

window.addEventListener('DOMContentLoaded', async () => {
const map = initMap();

try {
// Fixed 6-month window demo
const end = dayjs().format('YYYY-MM-DD');
const start = dayjs().subtract(6, 'month').format('YYYY-MM-DD');

// Persist center for buffer-based charts
const c = map.getCenter();
store.setCenterFromLngLat(c.lng, c.lat);
const merged = await getDistrictsMerged({ start, end });

map.on('load', () => {
const { breaks, colors } = renderDistrictChoropleth(map, merged);
drawLegend(breaks, colors, '#legend');
attachHover(map, 'districts-fill');
});
} catch (err) {
console.warn('Choropleth demo failed:', err);
}

// Wire points layer refresh with fixed 6-month filters for now
wirePoints(map, { getFilters: () => store.getFilters() });

// Charts: use same 6-month window and a default buffer at map center
try {
const { start, end, types, center3857, radiusM } = store.getFilters();
await updateAllCharts({ start, end, types, center3857, radiusM });
} catch (err) {
console.warn('Charts failed to render:', err);
const pane = document.getElementById('charts') || document.body;
const status = document.getElementById('charts-status') || (() => {
const d = document.createElement('div');
d.id = 'charts-status';
d.style.cssText = 'position:absolute;right:16px;top:16px;padding:8px 12px;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.1);background:#fff;font:14px/1.4 system-ui';
pane.appendChild(d);
return d;
})();
status.innerText = 'Charts unavailable: ' + (err.message || err);
}

// Controls panel
function refreshAll() {
const { start, end, types } = store.getFilters();
getDistrictsMerged({ start, end, types })
.then((merged) => {
if (map.isStyleLoaded()) {
const { breaks, colors } = renderDistrictChoropleth(map, merged);
drawLegend(breaks, colors, '#legend');
} else {
map.once('load', () => {
const { breaks, colors } = renderDistrictChoropleth(map, merged);
drawLegend(breaks, colors, '#legend');
});
}
})
.catch((e) => console.warn('Districts refresh failed:', e));

refreshPoints(map, { start, end, types }).catch((e) => console.warn('Points refresh failed:', e));

const f = store.getFilters();
updateAllCharts(f).catch((e) => {
console.error(e);
const pane = document.getElementById('charts') || document.body;
const status = document.getElementById('charts-status') || (() => {
const d = document.createElement('div');
d.id = 'charts-status';
d.style.cssText = 'position:absolute;right:16px;top:16px;padding:8px 12px;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.1);background:#fff;font:14px/1.4 system-ui';
pane.appendChild(d);
return d;
})();
status.innerText = 'Charts unavailable: ' + (e.message || e);
});

// Compare card (A vs B). Currently only A (store center); B is placeholder until UI for B exists.
updateCompare({
A: { center3857: store.center3857, radiusM: store.radius },
B: null,
types,
adminLevel: store.adminLevel,
timeWindow: store.timeWindowMonths,
}).catch((e) => console.warn('Compare update failed:', e));
}

initPanel(store, { onChange: refreshAll, getMapCenter: () => map.getCenter() });
});

Loading