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
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ services:
postgres:
container_name: map-postgres
image: postgis/postgis
platform: linux/amd64
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
Expand Down
44 changes: 22 additions & 22 deletions map/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,32 @@
from map.models import CommunityArea, RestaurantPermit


class CommunityAreaListSerializer(serializers.ListSerializer):
def to_representation(self, data):

result = {}
for item in data:
child_data = self.child.to_representation(item)
name = child_data["name"]
result[name] = {"area_id": child_data["area_id"], "num_permits": child_data["num_permits"]}
return [result]


class CommunityAreaSerializer(serializers.ModelSerializer):
class Meta:
model = CommunityArea
fields = ["name", "num_permits"]
fields = ["name", "area_id", "num_permits"]
list_serializer_class = CommunityAreaListSerializer

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
area_id = obj.area_id
num_permits = RestaurantPermit.objects.filter(
community_area_id=str(area_id),
issue_date__year=self.context["year"],
).count()
return num_permits



17 changes: 11 additions & 6 deletions map/static/js/App.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import React from "react"
import { createRoot } from "react-dom/client"
import React from "react";
import { createRoot } from "react-dom/client";

import RestaurantPermitMap from "./RestaurantPermitMap"
import RestaurantPermitMap from "./RestaurantPermitMap";

const container = document.getElementById("map")
const root = createRoot(container)
root.render(<RestaurantPermitMap />)
const container = document.getElementById("map");
const root = createRoot(container);
root.render(
// Would use ReactErrorBoundary library to catch errors anywhere in RestaurantPermitMap and display a fallback UI.
// <ReactErrorBoundary>
<RestaurantPermitMap />,
// </ReactErrorBoundary>
);
137 changes: 81 additions & 56 deletions map/static/js/RestaurantPermitMap.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
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 }) {
// Filter by the permit issue year for each restaurant
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 START_YEAR = 2026;

// Generates years from 2026 to 2016
const YEAR_OPTIONS = [...Array(11).keys()].map((increment) => {
return START_YEAR - increment;
});

function YearSelect({ setFilterVal }) {
return (
<>
<label htmlFor="yearSelect" className="fs-3">
Expand All @@ -30,80 +24,111 @@ function YearSelect({ setFilterVal }) {
className="form-select form-select-lg mb-3"
onChange={(e) => setFilterVal(e.target.value)}
>
{options}
{YEAR_OPTIONS.map((year) => (
<option key={year} value={year}>
{year}
</option>
))}
</select>
</>
)
);
}

export default function RestaurantPermitMap() {
const communityAreaColors = ["#eff3ff", "#bdd7e7", "#6baed6", "#2171b5"]
const communityAreaColors = ["#eff3ff", "#bdd7e7", "#6baed6", "#2171b5"];
const [error, setError] = useState(false);
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()
.then((res) => res.json())
setError(false);
fetch(yearlyDataEndpoint)
.then((res) => {
if (!res.ok) {
throw new Error("Failed to fetch data for year: " + year);
}
// Right here,we should also check that the data structure is valid using something like Zod or do it on the backend. Otherwise, it will break the app.
return res.json();
})
.then((data) => {
/**
* TODO: Fetch the data needed to supply to map with data
*/
setCurrentYearData(data[0]);
})
}, [yearlyDataEndpoint])
.catch((err) => {
// report error to something like Sentry
setError(true);
});
}, [yearlyDataEndpoint]);

const permitsIssuedThisYear = Object.values(currentYearData).reduce(
(acc, curr) => acc + curr.num_permits,
0,
);

const maxNumPermitsInSingleArea = Object.values(currentYearData).reduce(
(acc, curr) => Math.max(acc, curr.num_permits),
0,
);

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("")
layer.openPopup()
})
const permitData = currentYearData[feature.properties.community];
if (!permitData) return;

const numPermits = permitData.num_permits;
const percentageOfPermits = (numPermits / permitsIssuedThisYear) * 100;
const color = getColor(percentageOfPermits);

layer.setStyle({ fillColor: color });

layer.on("mouseover", () => {
layer.bindPopup(
`${numPermits} permits issued in ${feature.properties.community}<br>${percentageOfPermits.toFixed(2)}% of total permits`,
);
layer.openPopup();
});
}

return (
<>
{error ? (
<p className="fs-4 text-danger bg-danger-subtle p-3 rounded-3">
There was an error loading the data: Try selecting a different year.
</p>
) : null}
<YearSelect filterVal={year} setFilterVal={setYear} />
<p className="fs-4">
Restaurant permits issued this year: {/* TODO: display this value */}
Restaurant permits issued this year: {permitsIssuedThisYear}
</p>
<p className="fs-4">
Maximum number of restaurant permits in a single area:
{/* TODO: display this value */}
{maxNumPermitsInSingleArea}
</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 ? (
{Object.keys(currentYearData).length > 0 ? (
<GeoJSON
data={RAW_COMMUNITY_AREAS}
onEachFeature={setAreaInteraction}
key={maxNumPermits}
key={year}
/>
) : null}
</MapContainer>
</>
)
);
}
21 changes: 18 additions & 3 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
from map.models import CommunityArea, RestaurantPermit


EXPECTED_DATA = {
"Beverly": {"area_id": 1, "num_permits": 2},
"Lincoln Park": {"area_id": 2, "num_permits": 3},
}


@pytest.mark.django_db
def test_map_data_view():
# Create some test community areas
Expand Down Expand Up @@ -36,6 +42,15 @@ def test_map_data_view():
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
data = response.data

beverly_item = next((item for item in data if "Beverly" in item), None)
beverly_data = beverly_item["Beverly"] if beverly_item else None

lincoln_park_item = next((item for item in data if "Lincoln Park" in item), None)
lincoln_park_data = lincoln_park_item["Lincoln Park"] if lincoln_park_item else None

assert beverly_data is not None
assert lincoln_park_data is not None
assert beverly_data["num_permits"] == EXPECTED_DATA["Beverly"]["num_permits"]
assert lincoln_park_data["num_permits"] == EXPECTED_DATA["Lincoln Park"]["num_permits"]