rpmjp/portfolio
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>
  );
}