diff --git a/docker-compose.yml b/docker-compose.yml
index a743cce..1c541f5 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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
diff --git a/map/serializers.py b/map/serializers.py
index 03dd912..ee85857 100644
--- a/map/serializers.py
+++ b/map/serializers.py
@@ -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}
diff --git a/map/static/images/chicagoStars.svg b/map/static/images/chicagoStars.svg
new file mode 100644
index 0000000..68303e5
--- /dev/null
+++ b/map/static/images/chicagoStars.svg
@@ -0,0 +1,3 @@
+
diff --git a/map/static/images/chicagoStarsError.svg b/map/static/images/chicagoStarsError.svg
new file mode 100644
index 0000000..dd61240
--- /dev/null
+++ b/map/static/images/chicagoStarsError.svg
@@ -0,0 +1,9 @@
+
diff --git a/map/static/js/RestaurantPermitMap.js b/map/static/js/RestaurantPermitMap.js
index 57f8ea0..c998053 100644
--- a/map/static/js/RestaurantPermitMap.js
+++ b/map/static/js/RestaurantPermitMap.js
@@ -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 (
-
- )
- })
-
- return (
- <>
-
-
- >
- )
-}
+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 ;
}
+ 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 (
<>
-
-
- Restaurant permits issued this year: {/* TODO: display this value */}
-
-
- Maximum number of restaurant permits in a single area:
- {/* TODO: display this value */}
-
-
+ Chicago Restaurant Permits
+
+
+
+
+ {isLoading && }
+
{currentYearData.length > 0 ? (
-
+ <>
+
+ {activeArea && (
+ setActiveArea(null)}
+ >
+
+
+ )}
+
+ >
) : null}
>
- )
+ );
}
diff --git a/map/static/js/components/Error.jsx b/map/static/js/components/Error.jsx
new file mode 100644
index 0000000..bd12243
--- /dev/null
+++ b/map/static/js/components/Error.jsx
@@ -0,0 +1,38 @@
+const Error = () => {
+ const handleRefresh = () => {
+ window.location.reload();
+ };
+
+ return (
+
+
Oh no, something went wrong
+

+
+
+ );
+};
+
+export default Error;
diff --git a/map/static/js/components/LeafletPopUp.jsx b/map/static/js/components/LeafletPopUp.jsx
new file mode 100644
index 0000000..786c965
--- /dev/null
+++ b/map/static/js/components/LeafletPopUp.jsx
@@ -0,0 +1,10 @@
+const LeafletPopUp = ({ communityName, permits }) => {
+ return (
+
+
{communityName || "Name not available"}
+
{`Permits this year: ${permits ?? "not available"}`}
+
+ );
+};
+
+export default LeafletPopUp;
diff --git a/map/static/js/components/Loading.jsx b/map/static/js/components/Loading.jsx
new file mode 100644
index 0000000..e812038
--- /dev/null
+++ b/map/static/js/components/Loading.jsx
@@ -0,0 +1,28 @@
+const Loading = () => {
+ return (
+
+
Loading Data...
+

+
+ );
+};
+
+export default Loading;
diff --git a/map/static/js/components/MapLegend.jsx b/map/static/js/components/MapLegend.jsx
new file mode 100644
index 0000000..34dc5c7
--- /dev/null
+++ b/map/static/js/components/MapLegend.jsx
@@ -0,0 +1,64 @@
+import { createPortal } from "react-dom";
+import { useEffect, useState } from "react";
+import { useMap } from "react-leaflet";
+import L from "leaflet";
+
+const MapLegend = () => {
+ const map = useMap();
+ const [container, setContainer] = useState(null);
+ const grades = [0, 25, 50, 75];
+ const colors = ["#eff3ff", "#bdd7e7", "#6baed6", "#2171b5"];
+
+ useEffect(() => {
+ const legend = L.control({ position: "topright" });
+
+ legend.onAdd = () => {
+ const div = L.DomUtil.create("div", "info legend");
+ setContainer(div);
+ return div;
+ };
+
+ legend.addTo(map);
+ return () => legend.remove();
+ }, [map]);
+
+ return container
+ ? createPortal(
+
+
Permits (%)
+ {grades?.map((grade, i) => (
+
+
+
+ {grade}
+ {grades[i + 1] ? `-${grades[i + 1]}%` : "+%"}
+
+
+ ))}
+
,
+ container,
+ )
+ : null;
+};
+
+export default MapLegend;
diff --git a/map/static/js/components/YearSelect.jsx b/map/static/js/components/YearSelect.jsx
new file mode 100644
index 0000000..455c1ee
--- /dev/null
+++ b/map/static/js/components/YearSelect.jsx
@@ -0,0 +1,31 @@
+const YearSelect = ({ year, setYear }) => {
+ const startYear = 2026;
+ const years = [...Array(11).keys()].map((increment) => {
+ return startYear - increment;
+ });
+ const options = years.map((year) => {
+ return (
+
+ );
+ });
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export default YearSelect;
diff --git a/map/static/js/components/YearlyPermitInfo.jsx b/map/static/js/components/YearlyPermitInfo.jsx
new file mode 100644
index 0000000..f167565
--- /dev/null
+++ b/map/static/js/components/YearlyPermitInfo.jsx
@@ -0,0 +1,16 @@
+const YearlyPermitInfo = ({ totalPermits, maxPermits }) => {
+ const { max, name } = maxPermits;
+ return (
+
+
+ Restaurant permits issued this year: {totalPermits ?? "N/A"}
+
+
+ Maximum number of restaurant permits in a single area:{" "}
+ {`${max} (${name.join(", ")})` || "N/A"}
+
+
+ );
+};
+
+export default YearlyPermitInfo;
diff --git a/map/static/js/utils/generateAreaIdPermitObject.js b/map/static/js/utils/generateAreaIdPermitObject.js
new file mode 100644
index 0000000..fa4bb61
--- /dev/null
+++ b/map/static/js/utils/generateAreaIdPermitObject.js
@@ -0,0 +1,12 @@
+const generateAreaIdPermitObject = (mapData) => {
+ const map = {};
+
+ for (let res of mapData) {
+ const [_, value] = Object.entries(res)[0];
+ map[value.area_id] = value.num_permits;
+ }
+
+ return map;
+};
+
+export default generateAreaIdPermitObject;
diff --git a/map/static/js/utils/getMapAreaColor.js b/map/static/js/utils/getMapAreaColor.js
new file mode 100644
index 0000000..614fd97
--- /dev/null
+++ b/map/static/js/utils/getMapAreaColor.js
@@ -0,0 +1,23 @@
+const getMapAreaColor = (percentageOfPermits) => {
+ const communityAreaColors = ["#eff3ff", "#bdd7e7", "#6baed6", "#2171b5"];
+
+ if (typeof percentageOfPermits !== "number") {
+ return "#d3500a";
+ }
+
+ let fillColor;
+
+ if (percentageOfPermits < 25) {
+ fillColor = communityAreaColors[0];
+ } else if (percentageOfPermits >= 25 && percentageOfPermits < 50) {
+ fillColor = communityAreaColors[1];
+ } else if (percentageOfPermits >= 50 && percentageOfPermits < 75) {
+ fillColor = communityAreaColors[2];
+ } else {
+ fillColor = communityAreaColors[3];
+ }
+
+ return fillColor;
+};
+
+export default getMapAreaColor;
diff --git a/map/static/js/utils/getMaxNumberOfPermits.js b/map/static/js/utils/getMaxNumberOfPermits.js
new file mode 100644
index 0000000..e231290
--- /dev/null
+++ b/map/static/js/utils/getMaxNumberOfPermits.js
@@ -0,0 +1,20 @@
+const getMaxNumberOfPermits = (currentYearMapData) => {
+ let maxNumPermits = { max: 0, name: [] };
+
+ for (let data of currentYearMapData) {
+ const [key, value] = Object.entries(data)[0];
+
+ if (value.num_permits > maxNumPermits.max) {
+ maxNumPermits = {
+ max: value.num_permits,
+ name: [key], // reset
+ };
+ } else if (value.num_permits === maxNumPermits.max) {
+ maxNumPermits.name.push(key); // append
+ }
+ }
+
+ return maxNumPermits;
+};
+
+export default getMaxNumberOfPermits;
diff --git a/map/static/js/utils/getTotalPermitsPerYear.js b/map/static/js/utils/getTotalPermitsPerYear.js
new file mode 100644
index 0000000..0527853
--- /dev/null
+++ b/map/static/js/utils/getTotalPermitsPerYear.js
@@ -0,0 +1,9 @@
+const getTotalPermitsPerYear = (mapData) => {
+
+ return mapData.reduce((sum, areaObj) => {
+ const area = Object.values(areaObj)[0];
+ return sum + area.num_permits;
+ }, 0);
+};
+
+export default getTotalPermitsPerYear;
diff --git a/tests/test_views.py b/tests/test_views.py
index 24cc64e..ea294a4 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -32,10 +32,14 @@ def test_map_data_view():
community_area_id=area2.area_id, issue_date=date(2021, 6, 22)
)
- # 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.status_code == 200
+
+ data = response.json()
+ results = {list(item.keys())[0]: list(item.values())[0] for item in data}
+
+ assert results["Beverly"]["num_permits"] == 2
+ assert results["Beverly"]["area_id"] == 1
+ assert results["Lincoln Park"]["num_permits"] == 3
+ assert results["Lincoln Park"]["area_id"] == 2
diff --git a/webpack.config.js b/webpack.config.js
index cb6c7bb..7c3f288 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,6 +1,6 @@
-const path = require("path")
-const webpack = require("webpack") // eslint-disable-line no-unused-vars
-const BundleTracker = require("webpack-bundle-tracker")
+const path = require("path");
+const webpack = require("webpack"); // eslint-disable-line no-unused-vars
+const BundleTracker = require("webpack-bundle-tracker");
const config = {
context: __dirname,
@@ -46,7 +46,15 @@ const config = {
exclude: /node_modules/,
loader: "babel-loader",
options: {
- presets: ["@babel/preset-env", "@babel/preset-react"],
+ presets: [
+ "@babel/preset-env",
+ [
+ "@babel/preset-react",
+ {
+ runtime: "automatic",
+ },
+ ],
+ ],
},
},
{
@@ -99,7 +107,7 @@ const config = {
},
],
},
-}
+};
module.exports = (env, argv) => {
/*
@@ -109,12 +117,12 @@ module.exports = (env, argv) => {
* /app/static/bundles for bundles.
*/
if (argv.mode === "development") {
- config.output.publicPath = "http://localhost:3000/static/bundles/"
+ config.output.publicPath = "http://localhost:3000/static/bundles/";
}
if (argv.mode === "production") {
- config.output.publicPath = "/static/bundles/"
+ config.output.publicPath = "/static/bundles/";
}
- return config
-}
+ return config;
+};