rpmjp/projects/communityshield/map_fallback.tsx
CompletedMay – August 2025
CommunityShield
ML-powered crime pattern explorer for Chicago. 8.5M rows, 4 XGBoost models with SHAP explanations, beat-level heatmap, and an honest methodology page about what the data can and cannot tell you.
Python 3.12FastAPIPostgreSQL 16PostGISXGBoostSHAPReact 19MapLibre GL
Languages
TypeScript52.4%
Python41.8%
CSS3.2%
Other2.6%
map_fallback.tsx
/**
* MapFallback — the second tier of the 3-tier map rendering strategy.
*
* Strategy in priority order:
* 1. MapLibre GL (WebGL vector tiles) — default, rendered by CrimeMap.tsx,
* wrapped in MapErrorBoundary. Fails on browsers without WebGL or with
* hardware acceleration disabled.
* 2. Leaflet (DOM/Canvas raster) — this component. Works on any modern
* browser. Wrapped in another error boundary in case Leaflet itself
* throws (rare, but possible with corrupted tiles or extension
* interference).
* 3. Sortable table — the TableFallback below. Renders if both map
* libraries failed. Accessible by default, no rendering dependencies.
*
* The two error boundaries are deliberate: WebGL failures are caught at the
* CrimeMap level (outside this file) and route here; if THIS component also
* fails, the inner LeafletBoundary catches it and renders TableFallback
* without taking the whole page down.
*/
import { Component, type ReactNode } from "react";
import LeafletMap from "./LeafletMap";
import type { City, HeatmapFilters } from "../types";
interface Props {
filters: HeatmapFilters;
cities: City[];
selectedBeat: string | null;
onSelectBeat: (beatNumber: string | null) => void;
}
// Inner error boundary in case Leaflet also fails — fall through to table view.
// React error boundaries have to be class components; there's no hook equivalent
// for componentDidCatch.
class LeafletBoundary extends Component<
{ children: ReactNode; fallback: ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(err: Error) {
console.error("[LeafletBoundary]", err);
}
render() {
return this.state.hasError ? this.props.fallback : this.props.children;
}
}
export default function MapFallback(props: Props) {
return (
<LeafletBoundary fallback={<TableFallback />}>
<LeafletMap {...props} />
</LeafletBoundary>
);
}
function TableFallback() {
return (
<div className="h-full overflow-y-auto bg-brand-900 text-brand-50 p-6">
<div className="max-w-3xl mx-auto">
<div className="bg-amber-900/30 border border-amber-700 rounded-lg p-4 mb-6">
<div className="font-bold text-amber-200 mb-1">
Map view unavailable
</div>
<div className="text-sm text-amber-100/80">
Both interactive map renderers failed to load. The filters and prediction panel
are still available, but beat selection needs a browser with map rendering support.
</div>
</div>
<p className="text-brand-300 text-sm">
Reduced view. Try another browser, enable hardware acceleration, or check the console.
</p>
</div>
</div>
);
}