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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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.
12 changes: 10 additions & 2 deletions map/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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
return RestaurantPermit.objects.filter(
community_area_id=str(obj.area_id),
issue_date__year=year,
).count()
130 changes: 83 additions & 47 deletions map/static/js/RestaurantPermitMap.js
Original file line number Diff line number Diff line change
@@ -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 (
<option value={year} key={year}>
{year}
</option>
)
})
);
});

return (
<>
Expand All @@ -28,70 +28,106 @@ function YearSelect({ setFilterVal }) {
<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}
</select>
</>
)
);
}

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 toTitleCase(str) {
return str
.toLowerCase()
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}

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 = toTitleCase(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(
`<strong>${areaName}</strong><br/>Restaurant permits issued in ${year}: <strong>${permitCount}</strong><br/>Area share of citywide permits: <strong>${(permitShare * 100).toFixed(1)}%</strong>`,
);
layer.openPopup();
});

layer.on("mouseout", () => {
layer.closePopup();
});
}

return (
<>
<YearSelect filterVal={year} setFilterVal={setYear} />
<p className="fs-4">
Restaurant permits issued this year: {/* TODO: display this value */}
Restaurant permits issued this year: {totalPermits}
</p>
<p className="fs-4">
Maximum number of restaurant permits in a single area:
{/* TODO: display this value */}
Maximum number of restaurant permits in a single area: {maxNumPermits}
</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"
Expand All @@ -100,10 +136,10 @@ export default function RestaurantPermitMap() {
<GeoJSON
data={RAW_COMMUNITY_AREAS}
onEachFeature={setAreaInteraction}
key={maxNumPermits}
key={`${year}-${maxNumPermits}-${totalPermits}`}
/>
) : null}
</MapContainer>
</>
)
);
}
8 changes: 7 additions & 1 deletion tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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