n3xta (Mumu Li)

NYC Poop Map: a crowd-reported map, and the data behind itCase study

  • 4 minutes

A map where anyone can pin a dog-poop sighting in New York, rebuilt from a class project into a React and Firebase app, then extended into a scrolling data essay built on six NYC Open Data sets.

Role
Full-stack development
Stack
React, Firebase, D3, Leaflet, MapTiler, GSAP, Node.js, GitHub Actions
Live site
www.poop.nyc ↗
Kind
Project

Work in progress

Locked

This write-up isn’t finished, so it’s behind a password for now. If you’re a recruiter and don’t have it, ask me.

  • Role: Solo. Concept, design, front end, backend, data processing and deployment.
  • When: Spring 2025 (the map, for Networked Media) and Spring 2026 (the data essay, for Information Design)
  • Stack: React, React Router, Vite, Leaflet, MapTiler, Firebase (Firestore, Storage), D3, GSAP ScrollTrigger, Lenis, Node.js, nginx, GitHub Actions
  • Exhibited: NYC PIT Pop Up at the World Trade Center, April 2026
  • Live: poop.nyc (the map) and the data essay

The idea

The brief for Networked Media was a community website. I pitched a landing page for the IMA/ITP Open Source Club. Then I talked to the club president, and it turned out they didn’t need one. Neither did I. It felt painfully dull.

A few days later I stepped in dog poop on a Brooklyn sidewalk. Full weight, no warning. The shoe was finished, and the project was decided.

The Brooklyn Poop Map lets anyone pin a sighting. Its community, The Watch Your Step Society, is half fictional and half real: anyone who submits a pin is a member. The references were the San Francisco poop map and useless websites like paper toilet .com, which convinced me the site needed a counter that only goes up.

The home page, laid out like a newspaper front page on a grid background, with the sighting counter and a FAQ

A map that looks like a newspaper

I wanted the site to read as both a public service announcement and something weird enough to click. That meant black and white, inverted headlines, bordered buttons that flip to black on hover, and a graph-paper grid behind everything.

The default OpenStreetMap tiles broke that completely. Restyling them meant running a tile server or learning a styling toolchain, which was a lot of work for one look. I switched to MapTiler and its Toner style, a high-contrast black-and-white map originally from Stamen Design. It is one API call, and it looks printed.

The map page went through three layouts: cramped inside the site’s fixed-width column, then positioned absolutely and still wrong, then fullscreen like the San Francisco map, with the header hidden and navigation in a small floating panel. The whole point is to show the map.

The fullscreen Toner map of lower Manhattan and Brooklyn, with a floating panel for navigation and the report count

Reporting a sighting

A report is a location, a short description, an optional photo and a timestamp. Picking the location had to work on any device, so you can click the map to drop a marker or press “use current location” to use the browser’s geolocation. If that is denied, you drop the pin by hand. Each pin on the main map opens a popup with the description, the photo and when it was reported.

The first version ran on Express and EJS. Later I rebuilt it as a React single-page app with Firebase: reports go to Firestore, photos to Firebase Storage, and the app checks that the coordinates are real numbers and the description isn’t empty before anything is written. No server code of my own left to maintain.

One thing I never solved in the class version was locking the map to Brooklyn. Without it, the Brooklyn Poop Map is technically a worldwide poop map, and my friends in Shanghai could contribute too. It is now the NYC Poop Map.

From anecdotes to open data

Pins tell you where people stepped in something, not why. So I started a second piece for Information Design: a scrolling data essay on why New York sidewalks look the way they do.

A Node script reads six NYC Open Data exports (311 dog-waste complaints, the DSNY litter basket inventory, zip code boundaries, dog licenses, street cleanliness scorecard ratings and SweepNYC street cleaning logs) and writes compact JSON and GeoJSON for the browser. The 311 export covers every “Dirty Condition” complaint, so the script filters it down to dog waste. Locations arrive as WKT text, so I wrote small parsers for points and multipolygons instead of pulling in a GIS library. Points are stored as bare number arrays to keep the files small.

The essay runs about 18 sections, each with its own chart module loaded on demand. A few of them:

Section Data What it shows
Tale of two cities 311, zip boundaries Upper East Side and Brownsville side by side
Infrastructure desert 311, litter baskets A complaint heatmap, then every public basket, then the zips with the most reports per basket
Broken Windows cascade Scorecard ratings Districts with the steepest drop in sidewalk cleanliness
SweepNYC Sweep logs, 311 When sweepers work versus when complaints come in
MS4 runoff Harbor water samples Bacteria levels on wet days versus dry days

The layers are tied to scroll position. In the infrastructure section, the heatmap, the baskets and the ranked zips fade in one at a time as the reader reaches each paragraph, so the argument builds in the same order as the text. GSAP ScrollTrigger handles the timing, Lenis smooths the scrolling, and the maps reuse the same Toner basemap as the main app.

Shipping it

Both pieces deploy together. Pushing to main runs a GitHub Actions workflow that builds the essay and the React app, syncs them to a DigitalOcean droplet with rsync, then installs and reloads the nginx config. nginx serves the app, serves the essay under /poop-map-scrolly/, and caches static assets and data files for a year, since they only change on deploy.