Complete 2026 DataMade Code Challenge — Chicago Restaurant Permit Map #1
Complete 2026 DataMade Code Challenge — Chicago Restaurant Permit Map #1JESUSC1 wants to merge 11 commits into
Conversation
… year filtering, permit count API, and interactive community area popups. Adds backend aggregation query, DRF serializer, four pytest tests, and a React frontend with accessible color shading, pin-to-keep popups, and detailed implementation notes in the README.
…ote to the left and the interaction note to the right, flush with the map edges.
…how area name in caps with restaurant permits and year, and split map legend into two aligned notes.
Removed title and updated image link in README.
Added section for updated React map with image.
Updated note in README to reflect Django/React experience.
antidipyramid
left a comment
There was a problem hiding this comment.
Thanks for this very thorough PR, @JESUSC1. Left some comments for you inline.
| } | ||
|
|
||
| export default function RestaurantPermitMap() { | ||
| // Sequential blue palette (ColorBrewer 4-step) — accessible for color-blind users |
There was a problem hiding this comment.
Very thoughtful use of accessible colors.
There was a problem hiding this comment.
Thank you! I wanted the choropleth to be readable regardless of color perception differences. The single-hue blue scale (varying by lightness, not hue) holds up across the most common types of color blindness, and the orange hover border was chosen for the same reason: strong contrast against blue plus a non-color cue from the increased stroke weight.
| const [totalPermits, setTotalPermits] = useState(0) | ||
| const [maxNumPermits, setMaxNumPermits] = useState(0) | ||
| const [topArea, setTopArea] = useState(null) | ||
| const [loading, setLoading] = useState(false) |
There was a problem hiding this comment.
Since totalPermits, maxNumPermits, and topArea are all variables derived from the data returned from the server, what do you think about storing only the "raw" data from the server as a state variable?
The other values can then be computed as regular variables without triggering unnecessary re-renders.
There was a problem hiding this comment.
I see. Storing those as separate state variables causes three extra setState calls (and three extra re-renders) after every fetch. Since they're all derivable from currentYearData, they can be computed as plain variables each render cycle, instead should try:
const counts = currentYearData.map((area) => area.num_permits)
const totalPermits = counts.reduce((a, b) => a + b, 0)
const maxNumPermits = Math.max(...counts, 0)
const topArea = currentYearData.find((a) => a.num_permits === maxNumPermits)?.name ?? nullThat leaves only currentYearData, year, and loading as state variables, each tracking something that genuinely can't be derived. I'll make that change.
| return area1, area2 | ||
|
|
||
|
|
||
| # Required: verify the endpoint returns correct permit counts for a given year. |
There was a problem hiding this comment.
Thank you! The required test only covered the happy path, so I wanted to document some edge cases too.
|
|
||
| # Single aggregation query groups all permits by area for the selected year | ||
| # Result is sored in a dict for serrializer lookups to avoid N+1 queries | ||
| permit_counts = ( |
There was a problem hiding this comment.
Can you say more about how doing the calculation here in the view rather than in the serializer avoids n+1 queries?
There was a problem hiding this comment.
Sure! If the count lived inside the serializer, get_num_permits() would run something like RestaurantPermit.objects.filter(issue_date__year=year, community_area_id=area.area_id).count() once per community area. Chicago has 77 of them, so each API request would hit the database 77 times for counts alone, on top of the initial query to fetch the areas. That "one query per item in a loop" pattern is the N+1 problem.
Instead, the view runs a single aggregation query before the serializer is called:
permit_counts = (
RestaurantPermit.objects.filter(issue_date__year=year)
.values("community_area_id")
.annotate(count=Count("id"))
)
counts_by_area = {item["community_area_id"]: item["count"] for item in permit_counts}This is a single SELECT ... GROUP BY community_area_id at the database level, all 77 counts in one round trip. The result gets passed to the serializer as context, so get_num_permits() does a plain dict lookup with no database access at all. Two queries per request regardless of how many community areas exist, rather than N+1.
…render from currentYearData, and the useEffect only calls setCurrentYearData. State is down to currentYearData, year, and loading.
|
Resolved issues with uncesessary re-renders. The three derived values are now computed as plain variables on each render from currentYearData, and the useEffect only calls setCurrentYearData. State is down to currentYearData, year, and loading, see latest commit. |
Overview
Completes the DataMade 2026 code challenge by implementing a full-stack Django + React choropleth map visualizing Chicago restaurant permit data by community area and year.
Changes
MapDataViewwith single aggregation query to avoid N+1;CommunityAreaSerializerreturnsnum_permitsvia serializer context; returns 400 if?year=is missingtop area by name
Testing Instructions
docker compose builddocker compose run --rm app python manage.py loaddata map/fixtures/restaurant_permits.json map/fixtures/community_areas.jsondocker compose up— visit http://localhost:8000docker compose -f docker-compose.yml -f tests/docker-compose.yml run --rm app