Skip to content

Complete 2026 DataMade Code Challenge — Chicago Restaurant Permit Map #1

Open
JESUSC1 wants to merge 11 commits into
mainfrom
code_challenge_jc_2026_3_20
Open

Complete 2026 DataMade Code Challenge — Chicago Restaurant Permit Map #1
JESUSC1 wants to merge 11 commits into
mainfrom
code_challenge_jc_2026_3_20

Conversation

@JESUSC1
Copy link
Copy Markdown
Owner

@JESUSC1 JESUSC1 commented Mar 27, 2026

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

  • Backend: DRF MapDataView with single aggregation query to avoid N+1; CommunityAreaSerializer returns num_permits via serializer context; returns 400 if ?year= is missing
  • Tests: 4 pytest tests covering correct counts, year filter isolation, zero-permit areas, and missing year param
  • Frontend: Year filter (2016–2026), choropleth shading with accessible ColorBrewer blue scale, hover/click-to-pin popups with multiple simultaneous support, stats display showing total permits, max permits, and
    top area by name
  • Accessibility: Orange hover border (#ff7f00) for color-blind contrast, non-color weight cue, legend note below map
  • README: Implementation notes with reasoning for each step, data observations on permit distribution inequality, and future work

Testing Instructions

  • docker compose build
  • docker compose run --rm app python manage.py loaddata map/fixtures/restaurant_permits.json map/fixtures/community_areas.json
  • docker compose up — visit http://localhost:8000
  • Run tests: docker compose -f docker-compose.yml -f tests/docker-compose.yml run --rm app

JESUSC1 and others added 9 commits March 26, 2026 21:30
… 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.
@JESUSC1 JESUSC1 self-assigned this Mar 27, 2026
@JESUSC1 JESUSC1 added documentation Improvements or additions to documentation enhancement New feature or request labels Mar 27, 2026
Updated note in README to reflect Django/React experience.
Copy link
Copy Markdown

@antidipyramid antidipyramid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very thoughtful use of accessible colors.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Owner Author

@JESUSC1 JESUSC1 Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ?? null

That leaves only currentYearData, year, and loading as state variables, each tracking something that genuinely can't be derived. I'll make that change.

Comment thread tests/test_views.py
return area1, area2


# Required: verify the endpoint returns correct permit counts for a given year.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice extra tests!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! The required test only covered the happy path, so I wanted to document some edge cases too.

Comment thread map/views.py

# 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 = (
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you say more about how doing the calculation here in the view rather than in the serializer avoids n+1 queries?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@JESUSC1
Copy link
Copy Markdown
Owner Author

JESUSC1 commented Apr 19, 2026

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants