diff --git a/src/App.js b/src/App.js index ebae6bcf..d08caab8 100644 --- a/src/App.js +++ b/src/App.js @@ -16,22 +16,23 @@ import HowToHelp from "./Pages/HowToHelp"; const App = () => { return (
- Skip to content + + Skip to content +
diff --git a/src/Pages/InteractiveMap.js b/src/Pages/InteractiveMap.js index 9b116f5a..ef60f82d 100644 --- a/src/Pages/InteractiveMap.js +++ b/src/Pages/InteractiveMap.js @@ -1,22 +1,34 @@ import React from "react"; import Map from "../components/Map"; +import WardMap from "../components/WardMap"; const InteractiveMap = () => { return (
-

Interactive Map

- +

Interactive Maps

+ +
+

Bus Routes

+
    +
  • + Click a route or use the search bar to open route-specific details +
  • +
  • + Use the filter to compare the performance of different bus lines + across Chicago +
  • +
+ +
+ +
+

Wards

+

+ Click a ward to see the routes that pass through it. This map uses the 2023 ward boundaries. +

+ +
-

Welcome to the Map Beta!

-

- Use the map above to explore Chicago's bus lines. -

-

See a Bug? Have an Idea?

{" "} @@ -29,31 +41,42 @@ const InteractiveMap = () => { For a list of planned upcoming additions to this project, please visit our GitHub pages.

-

If you want to get involved, you can join our Tuesday Chi Hack Night breakout group.

+

+ If you want to get involved, you can join our Tuesday Chi Hack Night + breakout group. +

Finally, if you have feedback for us on this project, you can always reach out to us on Twitter!

- - + + - - + + - - + + - - + +
diff --git a/src/Routes/daily_july2022_cta_ridership_data.json b/src/Routes/daily_july2022_cta_ridership_data.json index a74374d7..08b18d4f 100644 --- a/src/Routes/daily_july2022_cta_ridership_data.json +++ b/src/Routes/daily_july2022_cta_ridership_data.json @@ -16806,4 +16806,4 @@ "day_type": "weekday", "rides": 4 } -] +] \ No newline at end of file diff --git a/src/Routes/july_2022_ridership (1).json b/src/Routes/july_2022_ridership (1).json index 00bbc6a7..d55df5c9 100644 --- a/src/Routes/july_2022_ridership (1).json +++ b/src/Routes/july_2022_ridership (1).json @@ -14055,4 +14055,4 @@ "952779": 3, "952780": 4 } -} +} \ No newline at end of file diff --git a/src/components/BusRouteDetails.js b/src/components/BusRouteDetails.js index e0459703..2b626276 100644 --- a/src/components/BusRouteDetails.js +++ b/src/components/BusRouteDetails.js @@ -21,7 +21,9 @@ function BusRouteDetails({ selectedRoute, busFraction }) { // FIX ME : eventually we're going to want these numbers rendered dynamically when the backend/data updates const percentileKeys = [63, 73, 75, 77, 80, 83, 87, 90, 93, 94]; - const percentileIndex = findPercentileIndex(selectedRoute[0]); + const percentileIndex = findPercentileIndex( + selectedRoute[0].properties.percentiles * 100 + ); const barGraphBars = percentileKeys.map((x, index) => { return (
))} - {[...Array(busFraction[0])].map((x) => ( - representation of CTA bus - ))} -
+ {[...Array(busFraction[0])].map((x) => ( + representation of CTA bus + ))} + ) : (
diff --git a/src/components/Filter.js b/src/components/Filter.js index 261c03d3..1bb9866f 100644 --- a/src/components/Filter.js +++ b/src/components/Filter.js @@ -1,10 +1,12 @@ import React from "react"; +import RankingLegend from "./RankingLegend"; const Filter = ({ currentFilters, setCurrentFilters, filterOpen, setFilterOpen, + wardFilter, }) => { return ( <> @@ -14,44 +16,65 @@ const Filter = ({ {" "} <> {" "} -

Reliability

- - + {!wardFilter && ( + <> +

Reliability

+ + + + )}

Map Settings

+ {!wardFilter && ( + + )}
- {currentFilters.color && ( -
-

Map Key by Percentile

- -
- )} + {currentFilters.color && } ); }; diff --git a/src/components/Header.js b/src/components/Header.js index 91e9f76b..e6a82720 100644 --- a/src/components/Header.js +++ b/src/components/Header.js @@ -27,8 +27,8 @@ export default function Header() { ); diff --git a/src/components/Map.js b/src/components/Map.js index 258fb3ed..452dcd96 100644 --- a/src/components/Map.js +++ b/src/components/Map.js @@ -6,7 +6,13 @@ import resultsData from "../Routes/data.json"; import Search from "./Search"; import Modal from "./Modal"; import Filter from "./Filter"; -import findPercentileIndex from "../utils/percentileKeys"; + +import { + highlightFeature, + resetHighlight, + setColor, + findDataForRoute, +} from "../utils/routeStyles"; export default function Map() { const [searchTerm, setSearchTerm] = useState(""); @@ -14,27 +20,24 @@ export default function Map() { const [filterOpen, setFilterOpen] = useState(false); const [currentFilters, setCurrentFilters] = useState({ + busLines: true, color: true, reliability: { top10: false, bottom10: false, }, }); + const { reliability } = currentFilters; // filter functionality const filterMapRoutes = (route) => { - if ( - !currentFilters.reliability.top10 && - !currentFilters.reliability.bottom10 - ) { + if (!reliability.top10 && !reliability.bottom10) { return true; } - const topTen = - !currentFilters.reliability.top10 || route.properties.ranking <= 10; - const bottomTen = - !currentFilters.reliability.bottom10 || route.properties.ranking >= 114; + const topTen = !reliability.top10 || route.properties.ranking <= 10; + const bottomTen = !reliability.bottom10 || route.properties.ranking >= 114; return topTen && bottomTen; }; @@ -76,22 +79,15 @@ export default function Map() { className="search-result" onClick={() => onClickBusRoute(result)} > -

{result.properties.route_id}{result.properties.route_long_name}

+

+ {result.properties.route_id} + {result.properties.route_long_name} +

)); - // modal functionality - // clicking a bus route opens the modal - function findDataForRoute(feature) { - const results = resultsData.features.filter( - (data) => - String(data.properties.route_id) === String(feature.properties.route_id) - ); - return results; - } - const onClickBusRoute = (feature) => { setSelectedRoute(findDataForRoute(feature)); document.body.style.overflow = "hidden"; @@ -113,24 +109,7 @@ export default function Map() { fillOpacity: 1, }; - const heatmap = ["#0852C1", "#8E47F3", "#D84091", "#EB4F12", "#FFED39"]; - - function setColor(route) { - const percentileIndex = findPercentileIndex(route); - if (percentileIndex === 0 || percentileIndex === 1) { - return heatmap[0]; - } else if (percentileIndex === 2 || percentileIndex === 3) { - return heatmap[1]; - } else if (percentileIndex === 4 || percentileIndex === 5) { - return heatmap[2]; - } else if (percentileIndex === 6 || percentileIndex === 7) { - return heatmap[3]; - } else { - return heatmap[4]; - } - } - - function onEachFeature(feature, layer) { + function onEachRouteFeature(feature, layer) { if (feature.properties) { const { route_long_name, route_id } = feature.properties; layer.bindTooltip(`${route_id}, ${route_long_name}`, { @@ -140,18 +119,19 @@ export default function Map() { layer.on({ click: () => onClickBusRoute(feature), mouseover: highlightFeature, - mouseout: resetHighlight, + mouseout: (e) => resetHighlight(e, currentFilters), }); const routeMatch = findDataForRoute(feature)[0]; + const routeMatchPercent = routeMatch.properties.percentiles * 100; routeMatch && layer.setStyle( currentFilters.color ? { weight: 4, - fillColor: setColor(routeMatch), - color: setColor(routeMatch), + fillColor: setColor(routeMatchPercent), + color: setColor(routeMatchPercent), fillOpacity: 1, } : style @@ -159,35 +139,8 @@ export default function Map() { } } - function highlightFeature(e) { - let layer = e.target; - - layer.setStyle({ - weight: 4, - fillColor: "#fff", - color: "#fff", - fillOpacity: 1, - }); - } - - function resetHighlight(e) { - let layer = e.target; - const routeMatch = findDataForRoute(layer.feature)[0]; - layer.setStyle( - currentFilters.color - ? { - color: setColor(routeMatch), - fillColor: setColor(routeMatch), - weight: 3, - fillOpacity: 1, - } - : style - ); - } - return (
-

Map/Data

{selectedRoute && ( )} @@ -212,12 +165,14 @@ export default function Map() { attribution='© Stadia Maps, © OpenMapTiles © OpenStreetMap contributors' url="https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png" /> - + {currentFilters.busLines && ( + + )}
); diff --git a/src/components/Nav.js b/src/components/Nav.js index 3dbcbca7..5bf6aa1d 100644 --- a/src/components/Nav.js +++ b/src/components/Nav.js @@ -8,7 +8,7 @@ const Nav = () => { const links = [ { title: "Home", link: "/" }, { title: "About Us", link: "/about" }, - { title: "Map", link: "/map" }, + { title: "Interactive Maps", link: "/map" }, { title: "Methods", link: "/methods" }, { title: "Further Readings", link: "/further-reading" }, { title: "How To Help", link: "/how-to-help" }, @@ -32,7 +32,7 @@ const Nav = () => { {links.map((link) => { return (
  • - + {link.title} diff --git a/src/components/RankingLegend.js b/src/components/RankingLegend.js new file mode 100644 index 00000000..32b425cc --- /dev/null +++ b/src/components/RankingLegend.js @@ -0,0 +1,34 @@ +import React from "react"; + +const RankingLegend = () => { + return ( +
    +

    Map Key by Percentile

    +
      +
    • + 0-19% + +
    • +
    • + 20-39% + +
    • +
    • + 40-59% + +
    • +
    • + 60-79% + +
    • +
    • + 80-99% + +
    • +
    +
    + ); +}; + + +export default RankingLegend; \ No newline at end of file diff --git a/src/components/WardMap.js b/src/components/WardMap.js new file mode 100644 index 00000000..dce4c5bb --- /dev/null +++ b/src/components/WardMap.js @@ -0,0 +1,230 @@ +import React, { useState } from "react"; +import { MapContainer, TileLayer, GeoJSON } from "react-leaflet"; + +import mapRoutes from "../Routes/bus_route_shapes_simplified_linestring.json"; +import resultsData from "../Routes/data.json"; +import wardRankings from "../Routes/ward_data.json"; +import Modal from "./Modal"; +import Filter from "./Filter"; + +import { + highlightFeature, + resetHighlight, + setColor, + findDataForRoute, +} from "../utils/routeStyles"; + +export default function WardMap() { + const [selectedRoute, setSelectedRoute] = useState(); + + const [filterOpen, setFilterOpen] = useState(false); + const [currentFilters, setCurrentFilters] = useState({ + busLines: false, + color: true, + wards: { + selectedWard: null, + wardsShowing: true, + }, + }); + const { wards } = currentFilters; + + const selectedWardFeature = wardRankings.features.find( + (feature) => feature.properties.ward === wards.selectedWard + ); + + // filter functionality + + const filterMapRoutes = (route) => { + if (!wards.selectedWard) { + return true; + } + + const wardMatches = + !wards.selectedWard || + selectedWardFeature.properties.routes.includes( + ` ${route.properties.route_id} ` + ); + return wardMatches; + }; + + const availableRoutes = resultsData.features + .filter(filterMapRoutes) + .map((route) => route.properties.route_id) + .filter((v, i, a) => a.indexOf(v) === i); + + const mapToDisplay = mapRoutes.features.filter((route) => + availableRoutes.includes(route.properties.route_id) + ); + + // modal functionality + + // clicking a bus route opens the modal + + const onClickBusRoute = (feature) => { + setSelectedRoute(findDataForRoute(feature)); + document.body.style.overflow = "hidden"; + }; + + const onClickWard = (feature) => { + debugger; + setCurrentFilters((prevfilters) => { + return { + ...prevfilters, + busLines: true, + wards: { + ...prevfilters.wards, + selectedWard: feature.properties.ward, + }, + }; + }); + }; + + const closeModal = () => { + setSelectedRoute(); + document.body.style.overflow = "scroll"; + }; + + //leaflet + + //highlight the hovered bus route + + const style = { + color: "rgb(51, 136, 255)", + fillColor: "rgb(51, 136, 255)", + weight: 3, + fillOpacity: 1, + }; + + function onEachRouteFeature(feature, layer) { + if (feature.properties) { + const { route_long_name, route_id } = feature.properties; + layer.bindTooltip(`${route_id}, ${route_long_name}`, { + sticky: true, + }); + + layer.on({ + click: () => onClickBusRoute(feature), + mouseover: highlightFeature, + mouseout: (e) => resetHighlight(e, currentFilters), + }); + + const routeMatch = + findDataForRoute(feature)[0].properties.percentiles * 100; + + routeMatch && + layer.setStyle( + currentFilters.color + ? { + weight: 4, + fillColor: setColor(routeMatch), + color: setColor(routeMatch), + fillOpacity: 1, + } + : style + ); + } + } + + const wardStyle = { + weight: 1, + fillOpacity: 0.2, + color: "white", + }; + + function onEachWardFeature(feature, layer) { + if (feature.properties) { + const { ward, median_percentiles } = feature.properties; + layer.bindTooltip(`Ward ${ward}`, { + sticky: true, + }); + layer.setStyle( + wards.selectedWard || !currentFilters.color + ? wardStyle + : { + ...wardStyle, + color: setColor(median_percentiles * 100), + } + ); + + layer.on({ + click: () => onClickWard(feature), + mouseover: highlightWard, + mouseout: resetHighlightWard, + }); + } + } + + function highlightWard(e) { + let layer = e.target; + + layer.setStyle({ + fillOpacity: 0.7, + }); + } + + function resetHighlightWard(e) { + let layer = e.target; + + layer.setStyle({ + fillOpacity: 0.2, + }); + } + + return ( +
    + {selectedRoute && ( + + )} + + + + + + {currentFilters.wards.wardsShowing && ( + + )} + {currentFilters.busLines && ( + + )} + +
    + ); +} diff --git a/src/css/main.css b/src/css/main.css index 3161095e..62cd309b 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -601,6 +601,15 @@ header .h1-container .subtitle-container img { min-width: 250px; } +.route-close-btn { + position: absolute; + right: 15px; + top: 15px; + z-index: 150; + padding: 10px; + border-radius: 10px; +} + .search-container { position: absolute; right: 15px; diff --git a/src/scss/_map.scss b/src/scss/_map.scss index 9f92ddc7..df21f4cf 100644 --- a/src/scss/_map.scss +++ b/src/scss/_map.scss @@ -23,3 +23,12 @@ min-width: 250px; } } + +.route-close-btn { + position: absolute; + right: 15px; + top: 15px; + z-index: 150; + padding: 10px; + border-radius: 10px; +} diff --git a/src/utils/percentileKeys.js b/src/utils/percentileKeys.js index b06014aa..96274049 100644 --- a/src/utils/percentileKeys.js +++ b/src/utils/percentileKeys.js @@ -1,11 +1,10 @@ - -export default function findPercentileIndex(route) { +export default function findPercentileIndex(percent) { const percentileIndex = Number( - (route.properties.percentiles * 100).toLocaleString("en-US", { + percent.toLocaleString("en-US", { minimumIntegerDigits: 2, useGrouping: false, })[0] ); - return percentileIndex + return percentileIndex; } diff --git a/src/utils/routeStyles.js b/src/utils/routeStyles.js new file mode 100644 index 00000000..72d2aa33 --- /dev/null +++ b/src/utils/routeStyles.js @@ -0,0 +1,62 @@ +import resultsData from "../Routes/data.json"; + +import findPercentileIndex from "./percentileKeys"; + +export function findDataForRoute(feature) { + const results = resultsData.features.filter( + (data) => + String(data.properties.route_id) === String(feature.properties.route_id) + ); + return results; +} + +const style = { + color: "rgb(51, 136, 255)", + fillColor: "rgb(51, 136, 255)", + weight: 3, + fillOpacity: 1, +}; + +const heatmap = ["#0852C1", "#8E47F3", "#D84091", "#EB4F12", "#FFED39"]; + +export function setColor(route) { + const percentileIndex = findPercentileIndex(route); + if (percentileIndex === 0 || percentileIndex === 1) { + return heatmap[0]; + } else if (percentileIndex === 2 || percentileIndex === 3) { + return heatmap[1]; + } else if (percentileIndex === 4 || percentileIndex === 5) { + return heatmap[2]; + } else if (percentileIndex === 6 || percentileIndex === 7) { + return heatmap[3]; + } else { + return heatmap[4]; + } +} + +export function highlightFeature(e) { + let layer = e.target; + + layer.setStyle({ + weight: 4, + fillColor: "#fff", + color: "#fff", + fillOpacity: 1, + }); +} + +export function resetHighlight(e, currentFilters) { + let layer = e.target; + const routeMatch = + findDataForRoute(layer.feature)[0].properties.percentiles * 100; + layer.setStyle( + currentFilters.color + ? { + color: setColor(routeMatch), + fillColor: setColor(routeMatch), + weight: 3, + fillOpacity: 1, + } + : style + ); +}