Skip to content
58 changes: 37 additions & 21 deletions map/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,48 @@
from map.models import CommunityArea, RestaurantPermit


"""
Serialize data, appending total number
of permits per community area

e.g. The endpoint /map-data/?year=2017 should return something like:
[
{
"ROGERS PARK": {
area_id: 17,
num_permits: 2
},
"BEVERLY": {
area_id: 72,
num_permits: 2
},
...
}
]
"""
class CommunityAreaSerializer(serializers.ModelSerializer):
class Meta:
model = CommunityArea
fields = ["name", "num_permits"]
fields = ["name", "area_id", "num_permits"]

num_permits = serializers.SerializerMethodField()

def get_num_permits(self, obj):
"""
TODO: 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:
[
{
"ROGERS PARK": {
area_id: 17,
num_permits: 2
},
"BEVERLY": {
area_id: 72,
num_permits: 2
},
...
}
]
"""

pass
return len(
RestaurantPermit.objects.filter(
issue_date__year=self.context["year"],
community_area_id=str(obj.area_id)
)
)

# Override to_representation to return data in the specified structure
def to_representation(self, obj):
data = super().to_representation(obj)
return {
data["name"]: {
"area_id": data["area_id"],
"num_permits": data["num_permits"]
}
}

175 changes: 130 additions & 45 deletions map/static/js/RestaurantPermitMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,65 +45,150 @@ 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(`Server error: ${res.status}`)
return res.json()
})
.then((data) => {
/**
* TODO: Fetch the data needed to supply to map with data
*/
setCurrentYearData(data)
})
.catch(error => {
console.error('Error fetching data:', error)
})
}, [yearlyDataEndpoint])

/**
* Add up number of permits per community area, returning citywide total for a given year
*/
const totalSum = currentYearData.reduce((accumulator, currentValue) => {
return accumulator + Object.values(currentValue)[0].num_permits;
}, 0)

/**
* Iterate through number of permits per community area, returning the maximum value found
*/
const maxNumPermits = currentYearData.reduce((accumulator, currentValue) => {
return accumulator > Object.values(currentValue)[0].num_permits ? accumulator : Object.values(currentValue)[0].num_permits;
}, 0)

/**
* Helper funcion for getColor. Computes percentage of permits
* per ward out of the max number of permits for a given year.
*/
function getPercentageOfPermits(communityPermits) {
if (maxNumPermits === 0){return 0}
return Math.round((communityPermits / maxNumPermits) * 100)
}

/**
* Splits percentages into 4 'buckets' corresponding
* to each array entry in communityAreaColors
* Bucket 1: | 0% - 24% | #eff3ff
* Bucket 2: | 25% - 49% | #bdd7e7
* Bucket 3: | 50% - 74% | #6baed6
* Bucket 4: | 75% - 100% | #2171b5
*/
function getColor(percentageOfPermits) {
/**
* TODO: Use this function in setAreaInteraction to set a community
* area's color using the communityAreaColors constant above
*/
if (percentageOfPermits < 25) {
return communityAreaColors[0]
} else if (percentageOfPermits <50) {
return communityAreaColors[1]
} else if (percentageOfPermits < 75) {
return communityAreaColors[2]
} else {
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("")

// Get community area object that corresponds to current geojson feature
const currentCommunityObj = currentYearData.find(communityArea => Object.keys(communityArea)[0] === feature.properties.community)
const communityPermits = Object.values(currentCommunityObj)[0].num_permits

const percentageOfPermits = getPercentageOfPermits(communityPermits)

layer.setStyle({color: 'black', weight: 1.5, fillColor: getColor(percentageOfPermits), fillOpacity: 1})
layer.on("click", () => {
layer.bindPopup(
`<b>${feature.properties.community}</b></br>
<span>Year: ${year}<span></br>
<span>Permits issued: ${communityPermits}</span>`)
layer.openPopup()
})
}

return (
<>
<YearSelect filterVal={year} setFilterVal={setYear} />
<p className="fs-4">
Restaurant permits issued this year: {/* TODO: display this value */}
</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}
>
<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}
<main>
<section aria-label="Filter controls">
<YearSelect filterVal={year} setFilterVal={setYear} />
</section>

<section aria-label="Summary statistics">
<p className="fs-4">
Restaurant permits issued this year: {totalSum}
</p>
<p className="fs-4">
Maximum number of restaurant permits in a single area:
{" "}{maxNumPermits}
</p>
</section>

<section aria-label="Map color legend" role="figure">
<h2 className="fs-6 fw-bold">Permits (% of max)</h2>
<ul style={{listStyle: "none", padding: 0}}>
<li>
<span aria-hidden="true"
style={{background: communityAreaColors[0],
padding: "0 8px"}}>&nbsp;
</span> 0–24%
</li>
<li>
<span aria-hidden="true"
style={{background: communityAreaColors[1],
padding: "0 8px"}}>&nbsp;
</span> 25–49%
</li>
<li>
<span aria-hidden="true"
style={{background: communityAreaColors[2],
padding: "0 8px"}}>&nbsp;
</span> 50–74%
</li>
<li>
<span aria-hidden="true"
style={{background: communityAreaColors[3],
padding: "0 8px"}}>&nbsp;
</span> 75–100%
</li>
</ul>
</section>

<section aria-label="Map of Chicago restaurant permits by community area">
<p aria-live="polite">
{currentYearData.length === 0 ?
"Loading map data..." : ""}
</p>
<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"
/>
) : null}
</MapContainer>
</>
{currentYearData.length > 0 ? (
<GeoJSON
data={RAW_COMMUNITY_AREAS}
onEachFeature={setAreaInteraction}
key={maxNumPermits}
/>
) : null}
</MapContainer>
</section>
</main>
)
}
10 changes: 5 additions & 5 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

from map.models import CommunityArea, RestaurantPermit


# assert that the /map-data/ endpoint
# returns the correct number of permits for Beverly and Lincoln
# Park in 2021
@pytest.mark.django_db
def test_map_data_view():
# Create some test community areas
Expand Down Expand Up @@ -35,7 +37,5 @@ def test_map_data_view():
# Query the map data endpoint
client = APIClient()
response = client.get(reverse("map_data", query={"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.data[0].get("Beverly").get("num_permits") == 2
assert response.data[1].get("Lincoln Park").get("num_permits") == 3