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

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")
permits = RestaurantPermit.objects.filter(community_area_id=str(obj.area_id))

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:
permits = permits.filter(issue_date__year=year)
return permits.count()

def to_representation(self, instance):
data = super().to_representation(instance)
name = data.pop("name")
return {name: data}
3 changes: 3 additions & 0 deletions map/static/images/chicagoStars.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions map/static/images/chicagoStarsError.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
202 changes: 118 additions & 84 deletions map/static/js/RestaurantPermitMap.js
Original file line number Diff line number Diff line change
@@ -1,109 +1,143 @@
import React, { useEffect, useState } from "react"
import { useEffect, useState } from "react";
import { MapContainer, TileLayer, GeoJSON, Popup } from "react-leaflet";

import { MapContainer, TileLayer, GeoJSON } from "react-leaflet"
import YearSelect from "./components/YearSelect";
import YearlyPermitInfo from "./components/YearlyPermitInfo";
import Loading from "./components/Loading";
import Error from "./components/Error";
import LeafletPopUp from "./components/LeafletPopUp";
import Legend from "./components/MapLegend";

import "leaflet/dist/leaflet.css"
import getMaxNumberOfPermits from "./utils/getMaxNumberOfPermits";
import getMapAreaColor from "./utils/getMapAreaColor";
import generateAreaIdPermitObject from "./utils/generateAreaIdPermitObject";
import getTotalPermitsPerYear from "./utils/getTotalPermitsPerYear";

import RAW_COMMUNITY_AREAS from "../../../data/raw/community-areas.geojson"
import "leaflet/dist/leaflet.css";

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>
)
})

return (
<>
<label htmlFor="yearSelect" className="fs-3">
Filter by year:{" "}
</label>
<select
id="yearSelect"
className="form-select form-select-lg mb-3"
onChange={(e) => setFilterVal(e.target.value)}
>
{options}
</select>
</>
)
}
import RAW_COMMUNITY_AREAS from "../../../data/raw/community-areas.geojson";

export default function RestaurantPermitMap() {
const communityAreaColors = ["#eff3ff", "#bdd7e7", "#6baed6", "#2171b5"]

const [currentYearData, setCurrentYearData] = useState([])
const [year, setYear] = useState(2026)
const [isLoading, setIsLoading] = useState(true);
const [error, setIsError] = useState(false);
const [activeArea, setActiveArea] = useState();
const [totalPermits, setTotalPermits] = useState(0);
const [currentYearData, setCurrentYearData] = useState([]);
const [areaIdMap, setAreaIdMap] = useState({});
const [year, setYear] = useState(2026);

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

useEffect(() => {
fetch()
.then((res) => res.json())
.then((data) => {
/**
* TODO: Fetch the data needed to supply to map with data
*/
})
}, [yearlyDataEndpoint])


function getColor(percentageOfPermits) {
/**
* TODO: Use this function in setAreaInteraction to set a community
* area's color using the communityAreaColors constant above
*/
const fetchMapData = async () => {
setIsError(false);
setIsLoading(true);
try {
const response = await fetch(yearlyDataEndpoint);

if (!response.ok) {
throw new Error("Network response was NOT ok");
}

const data = await response.json();
const areaIdObject = generateAreaIdPermitObject(data);
const totalPermitsForTheYear = getTotalPermitsPerYear(data);
setCurrentYearData(data);
setAreaIdMap(areaIdObject);
setTotalPermits(totalPermitsForTheYear);
} catch (err) {
console.error("Error details:", err);
setIsError(true);
} finally {
setIsLoading(false);
}
};

fetchMapData();
}, [year]);

if (error) {
return <Error />;
}

const maxNumPermits = getMaxNumberOfPermits(currentYearData);

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 communityName = feature.properties.community;
const area_id = feature.properties.area_num_1;
const permitPercentage =
maxNumPermits.max > 0
? (areaIdMap[area_id] / maxNumPermits.max) * 100
: 0;
const defaultStyle = {
fillColor: getMapAreaColor(permitPercentage),
weight: 1,
color: "red",
fillOpacity: 0.8,
};

const hoverStyle = {
...defaultStyle,
color: "green",
fillOpacity: 1,
};

layer.setStyle(defaultStyle);
layer.on({
mouseover: (e) => {
layer.setStyle(hoverStyle);
setActiveArea({
position: e.latlng,
name: communityName,
id: area_id,
color: "green",
});
},
mouseout: () => {
layer.setStyle(defaultStyle);
},
});
}

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}
>
<h1>Chicago Restaurant Permits</h1>
<YearSelect year={year} setYear={setYear} />

<YearlyPermitInfo
totalPermits={totalPermits}
maxPermits={maxNumPermits}
/>
<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"
/>
{isLoading && <Loading />}

{currentYearData.length > 0 ? (
<GeoJSON
data={RAW_COMMUNITY_AREAS}
onEachFeature={setAreaInteraction}
key={maxNumPermits}
/>
<>
<GeoJSON
data={RAW_COMMUNITY_AREAS}
onEachFeature={setAreaInteraction}
key={maxNumPermits.max}
/>
{activeArea && (
<Popup
position={activeArea?.position}
offset={[-0, -10]}
onClose={() => setActiveArea(null)}
>
<LeafletPopUp
communityName={activeArea.name}
permits={areaIdMap[activeArea.id]}
/>
</Popup>
)}
<Legend />
</>
) : null}
</MapContainer>
</>
)
);
}
38 changes: 38 additions & 0 deletions map/static/js/components/Error.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const Error = () => {
const handleRefresh = () => {
window.location.reload();
};

return (
<div
style={{
margin: "5rem 0",
display: "flex",
flexDirection: "column",
textAlign: "center",
alignItems: "center",
}}
>
<h1>Oh no, something went wrong</h1>
<img
src="static/images/chicagoStarsError.svg"
aria-hidden
width="200"
height="200"
/>
<button
onClick={handleRefresh}
style={{
width: "fit-content",
borderRadius: "8px",
background: "#41B6E6",
color: "white",
}}
>
Refresh page
</button>
</div>
);
};

export default Error;
10 changes: 10 additions & 0 deletions map/static/js/components/LeafletPopUp.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const LeafletPopUp = ({ communityName, permits }) => {
return (
<div>
<h2 className="fs-5">{communityName || "Name not available"}</h2>
<p>{`Permits this year: ${permits ?? "not available"}`}</p>
</div>
);
};

export default LeafletPopUp;
28 changes: 28 additions & 0 deletions map/static/js/components/Loading.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const Loading = () => {
return (
<div
style={{
position: "absolute",
height: "100vh",
inset: 0,
zIndex: 2000,
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(255, 255, 255, 0.7)",
backdropFilter: "blur(2px)",
}}
>
<h2>Loading Data...</h2>
<img
src="static/images/chicagoStars.svg"
alt="red stars found on Chicago flag"
width="100"
height="100"
/>
</div>
);
};

export default Loading;
Loading