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
34 changes: 13 additions & 21 deletions map/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,21 @@
class CommunityAreaSerializer(serializers.ModelSerializer):
class Meta:
model = CommunityArea
fields = ["name", "num_permits"]
fields = ["area_id","name", "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.

year = self.context.get("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
if year:
count = RestaurantPermit.objects.filter(
community_area_id=str(obj.area_id),
issue_date__year=year
).count()
return count
return 0


#
192 changes: 157 additions & 35 deletions map/static/js/RestaurantPermitMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "leaflet/dist/leaflet.css"

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 years = [...Array(11).keys()].map((increment) => {
Expand All @@ -28,6 +28,7 @@ function YearSelect({ setFilterVal }) {
<select
id="yearSelect"
className="form-select form-select-lg mb-3"
value={filterVal}
onChange={(e) => setFilterVal(e.target.value)}
>
{options}
Expand All @@ -41,69 +42,190 @@ export default function RestaurantPermitMap() {

const [currentYearData, setCurrentYearData] = useState([])
const [year, setYear] = useState(2026)
const [totalPermits, setTotalPermits] = useState(0)
const [maxPermits, setMaxPermits] = useState(0)
const [permitDataMap, setPermitDataMap] = useState({})
const [isLoading, setIsLoading] = useState(false)

const yearlyDataEndpoint = `/map-data/?year=${year}`

useEffect(() => {
fetch()
.then((res) => res.json())
setIsLoading(true)

// Fetch data when year changes
fetch(yearlyDataEndpoint)
.then((res) => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`)
}
return res.json()
})
.then((data) => {
/**
* TODO: Fetch the data needed to supply to map with data
*/
// Store the fetched data
setCurrentYearData(data)

// Create a mapping of area_id to permit count for quick lookup
const permitMap = {}
let total = 0
let max = 0

data.forEach(area => {
// Map area_id to permit count
permitMap[area.area_id] = area.num_permits
total += area.num_permits
if (area.num_permits > max) {
max = area.num_permits
}
})

setPermitDataMap(permitMap)
setTotalPermits(total)
setMaxPermits(max)
setIsLoading(false)
})
}, [yearlyDataEndpoint])

.catch(error => {
console.error("Error fetching permit data:", error)
setIsLoading(false)
// Optionally show error message to user
})
}, [year]) // Re-fetch when year changes

function getColor(percentageOfPermits) {
/**
* TODO: Use this function in setAreaInteraction to set a community
* area's color using the communityAreaColors constant above
*/
function getColor(areaId) {
// Get the permit count for this area
const permitCount = permitDataMap[areaId] || 0

// Calculate percentage of max permits
const percentageOfMax = maxPermits > 0 ? permitCount / maxPermits : 0

// Return color based on percentage
if (percentageOfMax === 0) return communityAreaColors[0]
if (percentageOfMax < 0.33) return communityAreaColors[1]
if (percentageOfMax < 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()
// Get area_id from the GeoJSON feature
// Note: Your GeoJSON might have area_id in different location
const areaId = feature.properties.area_id || feature.properties.AREA_ID || feature.properties.area_numbe

// Get permit count for this area
const permitCount = permitDataMap[areaId] || 0

// 1) Shade each community area according to what percentage of
// permits were issued there in the selected year
layer.setStyle({
fillColor: getColor(areaId),
fillOpacity: 0.7,
weight: 1,
color: "#333",
opacity: 0.8
})

// 2) On hover, display a popup with the community area's raw
// permit count for the year
layer.on({
mouseover: (e) => {
const layer = e.target
// Get area name from feature properties
const areaName = feature.properties.name || feature.properties.COMMUNITY || feature.properties.area_name
layer.bindPopup(`
<div style="font-family: Arial, sans-serif;">
<strong>${areaName}</strong><br>
<hr style="margin: 5px 0;">
<span>📊 Permits in ${year}: </span>
<strong>${permitCount}</strong>
</div>
`)
layer.openPopup()
},
mouseout: (e) => {
e.target.closePopup()
}
})
}

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>

{/* Display summary statistics */}
<div className="container mt-3 mb-3">
<div className="row">
<div className="col-md-6">
<div className="card bg-light">
<div className="card-body">
<h5 className="card-title">📋 Restaurant Permits</h5>
<p className="fs-4 mb-0">
{isLoading ? "Loading..." : totalPermits}
</p>
<small className="text-muted">Total permits issued in {year}</small>
</div>
</div>
</div>
<div className="col-md-6">
<div className="card bg-light">
<div className="card-body">
<h5 className="card-title">🏆 Maximum Permits</h5>
<p className="fs-4 mb-0">
{isLoading ? "Loading..." : maxPermits}
</p>
<small className="text-muted">Most permits in a single area in {year}</small>
</div>
</div>
</div>
</div>
</div>

{/* Map container */}
<MapContainer
id="restaurant-map"
center={[41.88, -87.62]}
zoom={10}
style={{ height: "600px", width: "100%" }}
>
<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 ? (
{RAW_COMMUNITY_AREAS && !isLoading && (
<GeoJSON
data={RAW_COMMUNITY_AREAS}
onEachFeature={setAreaInteraction}
key={maxNumPermits}
key={year} // Force re-render when year changes to update colors
/>
) : null}
)}
</MapContainer>

{/* Legend */}
<div className="container mt-3">
<div className="card">
<div className="card-body">
<h6 className="card-title">Color Legend</h6>
<div className="d-flex justify-content-between">
<div className="d-flex align-items-center">
<div style={{ width: "30px", height: "20px", backgroundColor: "#eff3ff", marginRight: "5px" }}></div>
<span>Lowest (0%)</span>
</div>
<div className="d-flex align-items-center">
<div style={{ width: "30px", height: "20px", backgroundColor: "#bdd7e7", marginRight: "5px" }}></div>
<span>Low (1-33%)</span>
</div>
<div className="d-flex align-items-center">
<div style={{ width: "30px", height: "20px", backgroundColor: "#6baed6", marginRight: "5px" }}></div>
<span>Medium (34-66%)</span>
</div>
<div className="d-flex align-items-center">
<div style={{ width: "30px", height: "20px", backgroundColor: "#2171b5", marginRight: "5px" }}></div>
<span>Highest (67-100%)</span>
</div>
</div>
<small className="text-muted mt-2 d-block">
Colors represent percentage of maximum permits in a single area
</small>
</div>
</div>
</div>
</>
)
}
}
2 changes: 1 addition & 1 deletion tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ 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
Expand Down