From b018fed6ae664eaf2f7aaf4e3834ead0353719ff Mon Sep 17 00:00:00 2001 From: NJ Smith Date: Tue, 17 Mar 2026 11:30:51 -0400 Subject: [PATCH 1/6] feat: open PR --- map/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/map/serializers.py b/map/serializers.py index 03dd912..197923f 100644 --- a/map/serializers.py +++ b/map/serializers.py @@ -31,4 +31,4 @@ def get_num_permits(self, obj): ] """ - pass + pass \ No newline at end of file From bf053c4b65e02f398713f6462e964919a548fd9b Mon Sep 17 00:00:00 2001 From: NJ Smith Date: Wed, 18 Mar 2026 13:26:25 -0400 Subject: [PATCH 2/6] feat: complete app --- map/serializers.py | 12 ++- map/static/js/RestaurantPermitMap.js | 122 ++++++++++++++++----------- tests/test_views.py | 8 +- 3 files changed, 92 insertions(+), 50 deletions(-) diff --git a/map/serializers.py b/map/serializers.py index 197923f..f3cd265 100644 --- a/map/serializers.py +++ b/map/serializers.py @@ -6,7 +6,7 @@ class CommunityAreaSerializer(serializers.ModelSerializer): class Meta: model = CommunityArea - fields = ["name", "num_permits"] + fields = ["name", "area_id", "num_permits"] num_permits = serializers.SerializerMethodField() @@ -30,5 +30,13 @@ def get_num_permits(self, obj): } ] """ + year = self.context.get("year") + try: + year = int(year) + except (TypeError, ValueError): + return 0 - pass \ No newline at end of file + return RestaurantPermit.objects.filter( + community_area_id=str(obj.area_id), + issue_date__year=year, + ).count() \ No newline at end of file diff --git a/map/static/js/RestaurantPermitMap.js b/map/static/js/RestaurantPermitMap.js index 57f8ea0..ff202ad 100644 --- a/map/static/js/RestaurantPermitMap.js +++ b/map/static/js/RestaurantPermitMap.js @@ -1,24 +1,24 @@ -import React, { useEffect, useState } from "react" +import React, { useEffect, useState } from "react"; -import { MapContainer, TileLayer, GeoJSON } from "react-leaflet" +import { MapContainer, TileLayer, GeoJSON } from "react-leaflet"; -import "leaflet/dist/leaflet.css" +import "leaflet/dist/leaflet.css"; -import RAW_COMMUNITY_AREAS from "../../../data/raw/community-areas.geojson" +import RAW_COMMUNITY_AREAS from "../../../data/raw/community-areas.geojson"; -function YearSelect({ setFilterVal }) { +function YearSelect({ filterVal, setFilterVal }) { // Filter by the permit issue year for each restaurant - const startYear = 2026 + const startYear = 2026; const years = [...Array(11).keys()].map((increment) => { - return startYear - increment - }) + return startYear - increment; + }); const options = years.map((year) => { return ( - ) - }) + ); + }); return ( <> @@ -28,70 +28,98 @@ function YearSelect({ setFilterVal }) { - ) + ); } export default function RestaurantPermitMap() { - const communityAreaColors = ["#eff3ff", "#bdd7e7", "#6baed6", "#2171b5"] + const communityAreaColors = ["#eff3ff", "#bdd7e7", "#6baed6", "#2171b5"]; - const [currentYearData, setCurrentYearData] = useState([]) - const [year, setYear] = useState(2026) + const [currentYearData, setCurrentYearData] = useState([]); + const [year, setYear] = useState(2026); - const yearlyDataEndpoint = `/map-data/?year=${year}` + const yearlyDataEndpoint = `/map-data/?year=${year}`; useEffect(() => { - fetch() + fetch(yearlyDataEndpoint) .then((res) => res.json()) .then((data) => { - /** - * TODO: Fetch the data needed to supply to map with data - */ + setCurrentYearData(data); }) - }, [yearlyDataEndpoint]) + .catch(() => { + setCurrentYearData([]); + }); + }, [yearlyDataEndpoint]); + const totalPermits = currentYearData.reduce( + (sum, area) => sum + area.num_permits, + 0, + ); + + const maxNumPermits = currentYearData.reduce( + (maxPermits, area) => Math.max(maxPermits, area.num_permits), + 0, + ); + + const permitsByAreaId = currentYearData.reduce((lookup, area) => { + lookup[String(area.area_id)] = area.num_permits; + return lookup; + }, {}); function getColor(percentageOfPermits) { - /** - * TODO: Use this function in setAreaInteraction to set a community - * area's color using the communityAreaColors constant above - */ + if (percentageOfPermits === 0) { + return communityAreaColors[0]; + } + if (percentageOfPermits <= 0.05) { + return communityAreaColors[1]; + } + if (percentageOfPermits <= 0.12) { + return communityAreaColors[2]; + } + + return communityAreaColors[3]; } function setAreaInteraction(feature, layer) { - /** - * TODO: Use the methods below to: - * 1) Shade each community area according to what percentage of - * permits were issued there in the selected year - * 2) On hover, display a popup with the community area's raw - * permit count for the year - */ - layer.setStyle() - layer.on("", () => { - layer.bindPopup("") - layer.openPopup() - }) + const areaName = feature.properties.community; + const areaId = feature.properties.area_numbe; + const permitCount = permitsByAreaId[String(areaId)] || 0; + const permitShare = totalPermits > 0 ? permitCount / totalPermits : 0; + + layer.setStyle({ + fillColor: getColor(permitShare), + fillOpacity: 0.7, + weight: 1, + opacity: 1, + }); + + layer.on("mouseover", () => { + layer.bindPopup( + `${areaName}
Permits in ${year}: ${permitCount}
Citywide share: ${(permitShare * 100).toFixed(1)}%`, + ); + layer.openPopup(); + }); + + layer.on("mouseout", () => { + layer.closePopup(); + }); } return ( <>

- Restaurant permits issued this year: {/* TODO: display this value */} + Restaurant permits issued this year: {totalPermits}

- Maximum number of restaurant permits in a single area: - {/* TODO: display this value */} + Maximum number of restaurant permits in a single area: {maxNumPermits}

- + ) : null} - ) + ); } diff --git a/tests/test_views.py b/tests/test_views.py index 24cc64e..80848c5 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -34,8 +34,14 @@ def test_map_data_view(): # Query the map data endpoint client = APIClient() - response = client.get(reverse("map_data", query={"year": 2021})) + response = client.get(reverse("map_data"), {"year": 2021}) # TODO: Complete the test by asserting that the /map-data/ endpoint # returns the correct number of permits for Beverly and Lincoln # Park in 2021 + assert response.status_code == 200 + + response_data = {item["name"]: item["num_permits"] for item in response.json()} + + assert response_data["Beverly"] == 2 + assert response_data["Lincoln Park"] == 3 From df2c67d82e8cbfcc63939c943d54d4bc905f82a6 Mon Sep 17 00:00:00 2001 From: NJ Smith Date: Mon, 23 Mar 2026 14:08:42 -0400 Subject: [PATCH 3/6] feat: bold neighborhoods in popup --- map/static/js/RestaurantPermitMap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/map/static/js/RestaurantPermitMap.js b/map/static/js/RestaurantPermitMap.js index ff202ad..205b4e6 100644 --- a/map/static/js/RestaurantPermitMap.js +++ b/map/static/js/RestaurantPermitMap.js @@ -100,7 +100,7 @@ export default function RestaurantPermitMap() { layer.on("mouseover", () => { layer.bindPopup( - `${areaName}
Permits in ${year}: ${permitCount}
Citywide share: ${(permitShare * 100).toFixed(1)}%`, + `${areaName}
Permits in ${year}: ${permitCount}
Citywide share: ${(permitShare * 100).toFixed(1)}%`, ); layer.openPopup(); }); From 38002acefd19e4d14f50ebae105190810d2c9715 Mon Sep 17 00:00:00 2001 From: NJ Smith Date: Mon, 23 Mar 2026 14:24:26 -0400 Subject: [PATCH 4/6] feat: add funciton to set readable case for area titles --- map/static/js/RestaurantPermitMap.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/map/static/js/RestaurantPermitMap.js b/map/static/js/RestaurantPermitMap.js index 205b4e6..430d881 100644 --- a/map/static/js/RestaurantPermitMap.js +++ b/map/static/js/RestaurantPermitMap.js @@ -85,8 +85,16 @@ export default function RestaurantPermitMap() { return communityAreaColors[3]; } + function toTitleCase(str) { + return str + .toLowerCase() + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + } + function setAreaInteraction(feature, layer) { - const areaName = feature.properties.community; + const areaName = toTitleCase(feature.properties.community); const areaId = feature.properties.area_numbe; const permitCount = permitsByAreaId[String(areaId)] || 0; const permitShare = totalPermits > 0 ? permitCount / totalPermits : 0; @@ -100,7 +108,7 @@ export default function RestaurantPermitMap() { layer.on("mouseover", () => { layer.bindPopup( - `${areaName}
Permits in ${year}: ${permitCount}
Citywide share: ${(permitShare * 100).toFixed(1)}%`, + `${areaName}
Permits issued in ${year}: ${permitCount}
Share of citywide permits: ${(permitShare * 100).toFixed(1)}%`, ); layer.openPopup(); }); From da7e420202bd1b59fa8e424b6f4c7a0a17b15e13 Mon Sep 17 00:00:00 2001 From: NJ Smith Date: Mon, 23 Mar 2026 14:48:59 -0400 Subject: [PATCH 5/6] feat: add Restaurant in tooltip --- README.md | 2 +- map/static/js/RestaurantPermitMap.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4f1a3b7..48c8724 100644 --- a/README.md +++ b/README.md @@ -89,4 +89,4 @@ _Note: If you would prefer to keep your code challenge private, please share acc | Xavier | https://github.com/xmedr | | Hayley | https://github.com/haowens | -Keep in mind that you cannot create a private fork of a public repository on GitHub, so you’ll need to [follow these instructions](https://gist.github.com/0xjac/85097472043b697ab57ba1b1c7530274) to create a private copy of the repo. \ No newline at end of file +Keep in mind that you cannot create a private fork of a public repository on GitHub, so you’ll need to [follow these instructions](https://gist.github.com/0xjac/85097472043b697ab57ba1b1c7530274) to create a private copy of the repo. diff --git a/map/static/js/RestaurantPermitMap.js b/map/static/js/RestaurantPermitMap.js index 430d881..b69e81a 100644 --- a/map/static/js/RestaurantPermitMap.js +++ b/map/static/js/RestaurantPermitMap.js @@ -108,7 +108,7 @@ export default function RestaurantPermitMap() { layer.on("mouseover", () => { layer.bindPopup( - `${areaName}
Permits issued in ${year}: ${permitCount}
Share of citywide permits: ${(permitShare * 100).toFixed(1)}%`, + `${areaName}
Restaurant permits issued in ${year}: ${permitCount}
Share of citywide permits: ${(permitShare * 100).toFixed(1)}%`, ); layer.openPopup(); }); From cf06e143301957d23462dd5a3a6670cd3f0f89e2 Mon Sep 17 00:00:00 2001 From: NJ Smith Date: Mon, 23 Mar 2026 15:02:10 -0400 Subject: [PATCH 6/6] feat: tooltip language --- map/static/js/RestaurantPermitMap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/map/static/js/RestaurantPermitMap.js b/map/static/js/RestaurantPermitMap.js index b69e81a..984d65c 100644 --- a/map/static/js/RestaurantPermitMap.js +++ b/map/static/js/RestaurantPermitMap.js @@ -108,7 +108,7 @@ export default function RestaurantPermitMap() { layer.on("mouseover", () => { layer.bindPopup( - `${areaName}
Restaurant permits issued in ${year}: ${permitCount}
Share of citywide permits: ${(permitShare * 100).toFixed(1)}%`, + `${areaName}
Restaurant permits issued in ${year}: ${permitCount}
Area share of citywide permits: ${(permitShare * 100).toFixed(1)}%`, ); layer.openPopup(); });