Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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'

16 changes: 2 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
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.
22 changes: 16 additions & 6 deletions map/serializers.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
from rest_framework import serializers
from rest_framework.exceptions import ValidationError

from map.models import CommunityArea, RestaurantPermit


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:
Expand All @@ -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.'))
145 changes: 90 additions & 55 deletions map/static/js/RestaurantPermitMap.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,31 @@
import React, { useEffect, useState } from "react"
import React, { useEffect, useState, useMemo } from "react"

import { MapContainer, TileLayer, GeoJSON } from "react-leaflet"

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 (
<option value={year} key={year}>
{year}
</option>
)
})
const years = [...Array(11).keys()].map((increment) => startYear - increment)

return (
<>
<label htmlFor="yearSelect" className="fs-3">
Filter by year:{" "}
Filter by year:
</label>
<select
id="yearSelect"
className="form-select form-select-lg mb-3"
onChange={(e) => setFilterVal(e.target.value)}
value={filterVal}
onChange={(e) => setFilterVal(Number(e.target.value))}
>
{options}
{years.map((year) => (
<option value={year} key={year}>
{year}
</option>
))}
</select>
</>
)
Expand All @@ -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(`<strong>${areaName}</strong><br/>Year: ${year} <br> Restaurant permits: ${numPermits}`)

layer.on("mouseover", () => layer.openPopup())
layer.on("mouseout", () => layer.closePopup())
}

return (
<>
<YearSelect filterVal={year} setFilterVal={setYear} />
<p className="fs-4">Restaurant permits issued this year: {totalNumPermits}</p>
<p className="fs-4">
Restaurant permits issued this year: {/* TODO: display this value */}
Maximum number of restaurant permits in a single area: {maxNumPermits}
</p>
<p className="fs-4">
Maximum number of restaurant permits in a single area:
{/* TODO: display this value */}
</p>
<MapContainer
id="restaurant-map"
center={[41.88, -87.62]}
zoom={10}
>

<MapContainer id="restaurant-map" center={[41.88, -87.62]} zoom={10}>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png"
/>
{currentYearData.length > 0 ? (
<GeoJSON
data={RAW_COMMUNITY_AREAS}
onEachFeature={setAreaInteraction}
key={maxNumPermits}
/>
) : null}
<GeoJSON
data={RAW_COMMUNITY_AREAS}
onEachFeature={setAreaInteraction}
key={`${year}-${maxNumPermits}-${totalNumPermits}`}
/>
</MapContainer>
</>
)
}
}
13 changes: 10 additions & 3 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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