diff --git a/Makefile b/Makefile index c3926e2..6a471cd 100644 --- a/Makefile +++ b/Makefile @@ -18,3 +18,14 @@ data/raw/chicago-restaurants.csv: data/raw/community-areas.geojson: curl https://data.cityofchicago.org/resource/igwz-8jzy.geojson -o $@ + +build: + docker compose build + +up: build + docker compose run --rm app python manage.py loaddata map/fixtures/restaurant_permits.json map/fixtures/community_areas.json + docker compose up -d + +down: + docker compose down --rmi 'all' + diff --git a/README.md b/README.md index 4f1a3b7..90fa1a9 100644 --- a/README.md +++ b/README.md @@ -21,19 +21,7 @@ Development requires a local installation of [Docker](https://docs.docker.com/ge Once you have Docker and Docker Compose installed, build the application containers from the project's root directory: ```bash -docker compose build -``` - -Load in the data: - -```bash -docker compose run --rm app python manage.py loaddata map/fixtures/restaurant_permits.json map/fixtures/community_areas.json -``` - -And finally, run the app: - -```bash -docker compose up +make up ``` The app will log to the console, and you should be able to visit it at http://localhost:8000 @@ -89,4 +77,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/serializers.py b/map/serializers.py index 03dd912..efd320c 100644 --- a/map/serializers.py +++ b/map/serializers.py @@ -1,4 +1,5 @@ from rest_framework import serializers +from rest_framework.exceptions import ValidationError from map.models import CommunityArea, RestaurantPermit @@ -6,13 +7,11 @@ class CommunityAreaSerializer(serializers.ModelSerializer): class Meta: model = CommunityArea - fields = ["name", "num_permits"] + fields = [] - num_permits = serializers.SerializerMethodField() - - def get_num_permits(self, obj): + def to_representation(self, obj: CommunityArea): """ - TODO: supplement each community area object with the number + Supplement each community area object with the number of permits issued in the given year. e.g. The endpoint /map-data/?year=2017 should return something like: @@ -30,5 +29,16 @@ def get_num_permits(self, obj): } ] """ + year = self.context.get("year") - pass + if year: + query = RestaurantPermit.objects.filter( + community_area_id=obj.area_id + ) + + return {obj.name : { + 'area_id' : obj.area_id, + 'num_permits' : query.count() + }} + else: + raise(ValidationError('Year not specified.')) \ No newline at end of file diff --git a/map/static/js/RestaurantPermitMap.js b/map/static/js/RestaurantPermitMap.js index 57f8ea0..ec47872 100644 --- a/map/static/js/RestaurantPermitMap.js +++ b/map/static/js/RestaurantPermitMap.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react" +import React, { useEffect, useState, useMemo } from "react" import { MapContainer, TileLayer, GeoJSON } from "react-leaflet" @@ -6,31 +6,26 @@ import "leaflet/dist/leaflet.css" import RAW_COMMUNITY_AREAS from "../../../data/raw/community-areas.geojson" -function YearSelect({ setFilterVal }) { - // Filter by the permit issue year for each restaurant +function YearSelect({ filterVal, setFilterVal }) { const startYear = 2026 - const years = [...Array(11).keys()].map((increment) => { - return startYear - increment - }) - const options = years.map((year) => { - return ( - - ) - }) + const years = [...Array(11).keys()].map((increment) => startYear - increment) return ( <> ) @@ -45,65 +40,105 @@ export default function RestaurantPermitMap() { const yearlyDataEndpoint = `/map-data/?year=${year}` useEffect(() => { - fetch() - .then((res) => res.json()) + fetch(yearlyDataEndpoint) + .then((res) => { + if (!res.ok) { + throw new Error(`Request failed: ${res.status}`) + } + return res.json() + }) .then((data) => { - /** - * TODO: Fetch the data needed to supply to map with data - */ + console.log("API data:", data) + setCurrentYearData(Array.isArray(data) ? data : []) + }) + .catch((err) => { + console.error("Error fetching map data:", err) + setCurrentYearData([]) }) }, [yearlyDataEndpoint]) + const areaPermitMap = useMemo(() => { + const mapped = {} + + if (!Array.isArray(currentYearData)) return mapped + + currentYearData.forEach((item) => { + if (!item || typeof item !== "object") return + + const keys = Object.keys(item) + if (keys.length === 0) return + + const areaName = keys[0] + mapped[areaName] = item[areaName] + }) + + console.log("areaPermitMap:", mapped) + return mapped + }, [currentYearData]) + + const totalNumPermits = useMemo(() => { + return Object.values(areaPermitMap).reduce( + (sum, area) => sum + (area?.num_permits ?? 0), + 0 + ) + }, [areaPermitMap]) + + const maxNumPermits = useMemo(() => { + const permitCounts = Object.values(areaPermitMap).map( + (area) => area?.num_permits ?? 0 + ) + return permitCounts.length > 0 ? Math.max(...permitCounts) : 0 + }, [areaPermitMap]) 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.33) return communityAreaColors[1] + if (percentageOfPermits <= 0.66) 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() + console.log("GeoJSON feature:", feature) + + const areaName = feature?.properties?.community ?? "Unknown" + const areaData = areaPermitMap[areaName] + const numPermits = areaData?.num_permits ?? 0 + + const percentageOfPermits = + maxNumPermits > 0 ? numPermits / maxNumPermits : 0 + + layer.setStyle({ + fillColor: getColor(percentageOfPermits), + fillOpacity: 0.7, + color: "#333", + weight: 1, }) - } + layer.bindPopup(`${areaName}
Year: ${year}
Restaurant permits: ${numPermits}`) + + layer.on("mouseover", () => layer.openPopup()) + layer.on("mouseout", () => layer.closePopup()) + } + return ( <> +

Restaurant permits issued this year: {totalNumPermits}

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

-

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

- + + - {currentYearData.length > 0 ? ( - - ) : null} + ) -} +} \ No newline at end of file diff --git a/tests/test_views.py b/tests/test_views.py index 24cc64e..7cb07c8 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -35,7 +35,14 @@ def test_map_data_view(): # Query the map data endpoint client = APIClient() response = client.get(reverse("map_data", query={"year": 2021})) + + resp1, resp2 = tuple(response.data) - # 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 area1.name in resp1.keys() + assert area2.name in resp2.keys() + + assert area1.area_id == str(resp1[area1.name]["area_id"]) + assert area2.area_id == str(resp2[area2.name]["area_id"]) + + assert resp1[area1.name]["num_permits"] == 2 + assert resp2[area2.name]["num_permits"] == 3 \ No newline at end of file