Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wards + ability to click to see routes in ward #54

Closed
wants to merge 9 commits into from
Prev Previous commit
Next Next commit
refactor: combine ward filtering onto map page
amy-corson committed Feb 24, 2023
commit afe66af2c819c7326922a354d23e100fa9f03d30
21 changes: 10 additions & 11 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -9,31 +9,30 @@ import Nav from "./components/Nav";
import Home from "./Pages/Home";
import About from "./Pages/About";
import InteractiveMap from "./Pages/InteractiveMap";
import InteractiveWardMap from "./Pages/InteractiveWardMap";
import FurtherReading from "./Pages/FurtherReading";
import Methods from "./Pages/Methods";
import HowToHelp from "./Pages/HowToHelp";

const App = () => {
return (
<div className="App">
<a className="skip-link" href="#main">Skip to content</a>
<a className="skip-link" href="#main">
Skip to content
</a>
<Nav />
<div className="container">
<Socials />
<Header />
<main id="main">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/map" element={<InteractiveMap />} />
<Route path="/ward-map" element={<InteractiveWardMap />} />
<Route path="/further-reading" element={<FurtherReading />} />
<Route path="/methods" element={<Methods />} />
<Route path="/how-to-help" element={<HowToHelp />} />
</Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/map" element={<InteractiveMap />} />
<Route path="/further-reading" element={<FurtherReading />} />
<Route path="/methods" element={<Methods />} />
<Route path="/how-to-help" element={<HowToHelp />} />
</Routes>
</main>

</div>
<Footer />
</div>
68 changes: 43 additions & 25 deletions src/Pages/InteractiveMap.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import React from "react";
import Map from "../components/Map";
import WardMap from "../components/WardMap";

const InteractiveMap = () => {
return (
<div className="page-container">
<h1>Interactive Map</h1>
<h1>Interactive Maps</h1>

<h2>Routes</h2>
<Map />

<h2>Wards</h2>
<WardMap />

<h2>Welcome to the Map Beta!</h2>
<p>
Use the map above to explore Chicago's bus lines.
</p>
<p>Use the map above to explore Chicago's bus lines.</p>
<ul>
<li>Click a
route or use the search bar to open route-specific details.</li>
<li>Use the filter to compare the performance of different bus lines
across Chicago.</li>
<li>
Click a route or use the search bar to open route-specific details.
</li>
<li>
Use the filter to compare the performance of different bus lines
across Chicago.
</li>
</ul>
<h3> See a Bug? Have an Idea?</h3>
<p>
@@ -29,31 +36,42 @@ const InteractiveMap = () => {
For a list of planned upcoming additions to this project, please visit
our GitHub pages.
</p>
<p>If you want to get involved, you can join our Tuesday Chi Hack Night breakout group.</p>
<p>
If you want to get involved, you can join our Tuesday Chi Hack Night
breakout group.
</p>
<p>
Finally, if you have feedback for us on this project, you can always
reach out to us on Twitter!
</p>
<div className="btn-container">
<a href="https://github.com/chihacknight/breakout-groups/issues/217" target="_blank" rel="noreferrer">
<button className="action-btn">
Breakout group information
</button>
<a
href="https://github.com/chihacknight/breakout-groups/issues/217"
target="_blank"
rel="noreferrer"
>
<button className="action-btn">Breakout group information</button>
</a>
<a href="https://github.com/chihacknight/ghost-buses-frontend" target="_blank" rel="noreferrer">
<button className="action-btn">
Frontend Repository
</button>
<a
href="https://github.com/chihacknight/ghost-buses-frontend"
target="_blank"
rel="noreferrer"
>
<button className="action-btn">Frontend Repository</button>
</a>
<a href="https://github.com/chihacknight/chn-ghost-buses/" target="_blank" rel="noreferrer">
<button className="action-btn">
Data Repository
</button>
<a
href="https://github.com/chihacknight/chn-ghost-buses/"
target="_blank"
rel="noreferrer"
>
<button className="action-btn">Data Repository</button>
</a>
<a href="https://twitter.com/ghostbuses" target="_blank" rel="noreferrer">
<button className="action-btn">
Ghost Bus Twitter
</button>
<a
href="https://twitter.com/ghostbuses"
target="_blank"
rel="noreferrer"
>
<button className="action-btn">Ghost Bus Twitter</button>
</a>
</div>
</div>
66 changes: 0 additions & 66 deletions src/Pages/InteractiveWardMap.js

This file was deleted.

12 changes: 7 additions & 5 deletions src/components/BusRouteDetails.js
Original file line number Diff line number Diff line change
@@ -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 (
<div
@@ -95,10 +97,10 @@ function BusRouteDetails({ selectedRoute, busFraction }) {
<img src={busIcon} alt="representation of CTA bus" />
))}

{[...Array(busFraction[0])].map((x) => (
<img src={ghostIcon} alt="representation of CTA bus" />
))}
</div>
{[...Array(busFraction[0])].map((x) => (
<img src={ghostIcon} alt="representation of CTA bus" />
))}
</div>
</div>
) : (
<div className="grid-square bus-graphic">
115 changes: 60 additions & 55 deletions src/components/Filter.js
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ const Filter = ({
setCurrentFilters,
filterOpen,
setFilterOpen,
wardFilter,
}) => {
return (
<>
@@ -15,59 +16,65 @@ const Filter = ({
{" "}
<>
{" "}
<h4>Reliability</h4>
<label>
<input
type="checkbox"
checked={currentFilters.reliability.top10}
onChange={() =>
setCurrentFilters((prevfilters) => {
return {
...prevfilters,
reliability: {
...prevfilters.reliability,
top10: !prevfilters.reliability.top10,
},
};
})
}
/>
Top 10 Buses
</label>
<label>
<input
type="checkbox"
checked={currentFilters.reliability.bottom10}
onChange={() =>
setCurrentFilters((prevfilters) => {
return {
...prevfilters,
reliability: {
...prevfilters.reliability,
bottom10: !prevfilters.reliability.bottom10,
},
};
})
}
/>
Bottom 10 buses
</label>
{!wardFilter && (
<>
<h4>Reliability</h4>
<label>
<input
type="checkbox"
checked={currentFilters.reliability.top10}
onChange={() =>
setCurrentFilters((prevfilters) => {
return {
...prevfilters,
reliability: {
...prevfilters.reliability,
top10: !prevfilters.reliability.top10,
},
};
})
}
/>
Top 10 Buses
</label>
<label>
<input
type="checkbox"
checked={currentFilters.reliability.bottom10}
onChange={() =>
setCurrentFilters((prevfilters) => {
return {
...prevfilters,
reliability: {
...prevfilters.reliability,
bottom10: !prevfilters.reliability.bottom10,
},
};
})
}
/>
Bottom 10 buses
</label>
</>
)}
<h4>Map Settings</h4>
<label>
<input
type="checkbox"
checked={currentFilters.busLines}
onChange={() =>
setCurrentFilters((prevfilters) => {
return {
...prevfilters,
busLines: !prevfilters.busLines,
};
})
}
/>
Show Bus Routes
</label>
{!wardFilter && (
<label>
<input
type="checkbox"
checked={currentFilters.busLines}
onChange={() =>
setCurrentFilters((prevfilters) => {
return {
...prevfilters,
busLines: !prevfilters.busLines,
};
})
}
/>
Show Bus Routes
</label>
)}
<label>
<input
type="checkbox"
@@ -93,9 +100,7 @@ const Filter = ({
<i aria-hidden="true" class="fa-solid fa-filter"></i>
</button>
</div>
{currentFilters.color && (
<RankingLegend/>
)}
{currentFilters.color && <RankingLegend />}
</>
);
};
71 changes: 12 additions & 59 deletions src/components/Map.js
Original file line number Diff line number Diff line change
@@ -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("");
@@ -21,7 +27,7 @@ export default function Map() {
bottom10: false,
},
});
const {reliability} = currentFilters;
const { reliability } = currentFilters;

// filter functionality

@@ -80,24 +86,13 @@ export default function Map() {
</div>
));

// 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";
};


const closeModal = () => {
setSelectedRoute();
document.body.style.overflow = "scroll";
@@ -114,23 +109,6 @@ 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 onEachRouteFeature(feature, layer) {
if (feature.properties) {
const { route_long_name, route_id } = feature.properties;
@@ -141,51 +119,26 @@ 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
);
}
}

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 (
<div className="map">
<h2>Map/Data</h2>
5 changes: 2 additions & 3 deletions src/components/Nav.js
Original file line number Diff line number Diff line change
@@ -8,8 +8,7 @@ const Nav = () => {
const links = [
{ title: "Home", link: "/" },
{ title: "About Us", link: "/about" },
{ title: "Bus Route Map", link: "/map" },
{ title: "Ward Map", link: "/ward-map" },
{ title: "Interactive Maps", link: "/map" },
{ title: "Methods", link: "/methods" },
{ title: "Further Readings", link: "/further-reading" },
{ title: "How To Help", link: "/how-to-help" },
@@ -33,7 +32,7 @@ const Nav = () => {
{links.map((link) => {
return (
<li key={link.link} className="nav-link">
<img src={busNavIcon} alt="" />
<img src={busNavIcon} alt="" />
<Link onClick={toggle} to={link.link}>
{link.title}
</Link>
54 changes: 0 additions & 54 deletions src/components/WardFilter.js

This file was deleted.

166 changes: 51 additions & 115 deletions src/components/WardMap.js
Original file line number Diff line number Diff line change
@@ -4,29 +4,29 @@ 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 Search from "./Search";
import Modal from "./Modal";
import WardFilter from "./WardFilter";
import findPercentileIndex from "../utils/percentileKeys";
import Filter from "./Filter";

import {
highlightFeature,
resetHighlight,
setColor,
findDataForRoute,
} from "../utils/routeStyles";

export default function WardMap() {
const [searchTerm, setSearchTerm] = useState("");
const [selectedRoute, setSelectedRoute] = useState();

const [filterOpen, setFilterOpen] = useState(false);
const [currentFilters, setCurrentFilters] = useState({
busLines: false,
color: true,
reliability: {
top10: false,
bottom10: false,
},
wards: {
selectedWard: null,
wardsShowing: true,
},
});
const { reliability, wards } = currentFilters;
const { wards } = currentFilters;

const selectedWardFeature = wardRankings.features.find(
(feature) => feature.properties.ward === wards.selectedWard
@@ -35,18 +35,16 @@ export default function WardMap() {
// filter functionality

const filterMapRoutes = (route) => {
if (!reliability.top10 && !reliability.bottom10 && !wards.selectedWard) {
if (!wards.selectedWard) {
return true;
}

const topTen = !reliability.top10 || route.properties.ranking <= 10;
const bottomTen = !reliability.bottom10 || route.properties.ranking >= 114;
const wardMatches =
!wards.selectedWard ||
selectedWardFeature.properties.routes.includes(
` ${route.properties.route_id} `
);
return topTen && bottomTen && wardMatches;
return wardMatches;
};

const availableRoutes = resultsData.features
@@ -58,60 +56,17 @@ export default function WardMap() {
availableRoutes.includes(route.properties.route_id)
);

//search functionality

const onChangeSearch = (e) => {
setSearchTerm(e.target.value.toLowerCase());
};

const searchResults = resultsData.features
.filter((route) => {
return (
String(route.properties.route_id) +
route.properties.route_long_name.toLowerCase()
).includes(searchTerm);
})
.filter((route) => {
return !route.properties.direction.includes("South");
})
.filter((route) => {
return !route.properties.direction.includes("West");
})
.filter((route) => {
return route.properties.day_type === "wk";
});

const searchResultsElements = searchResults.map((result) => (
<div
key={result.id}
className="search-result"
onClick={() => onClickBusRoute(result)}
>
<p>
<span>{result.properties.route_id}</span>
{result.properties.route_long_name}
</p>
</div>
));

// 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";
};

const onClickWard = (feature) => {
debugger;
setCurrentFilters((prevfilters) => {
return {
...prevfilters,
@@ -140,23 +95,6 @@ export default function WardMap() {
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 onEachRouteFeature(feature, layer) {
if (feature.properties) {
const { route_long_name, route_id } = feature.properties;
@@ -167,10 +105,11 @@ export default function WardMap() {
layer.on({
click: () => onClickBusRoute(feature),
mouseover: highlightFeature,
mouseout: resetHighlight,
mouseout: (e) => resetHighlight(e, currentFilters),
});

const routeMatch = findDataForRoute(feature)[0];
const routeMatch =
findDataForRoute(feature)[0].properties.percentiles * 100;

routeMatch &&
layer.setStyle(
@@ -186,18 +125,27 @@ export default function WardMap() {
}
}

const wardStyle = {
weight: 1,
fillOpacity: 0.2,
dashArray: 5,
color: "white",
};

function onEachWardFeature(feature, layer) {
if (feature.properties) {
const { ward } = feature.properties;
const { ward, median_percentiles } = feature.properties;
layer.bindTooltip(`Ward ${ward}`, {
sticky: true,
});
layer.setStyle({
weight: 1,
color: "#fff",
fillOpacity: 0.2,
dashArray: 5,
});
layer.setStyle(
wards.selectedWard || !currentFilters.color
? wardStyle
: {
...wardStyle,
color: setColor(median_percentiles * 100),
}
);

layer.on({
click: () => onClickWard(feature),
@@ -223,32 +171,6 @@ export default function WardMap() {
});
}

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 (
<div className="map">
{selectedRoute && (
@@ -259,16 +181,29 @@ export default function WardMap() {
zoom={11}
scrollWheelZoom={false}
>
<WardFilter
<button
className="route-close-btn"
onClick={() =>
setCurrentFilters((prevfilters) => {
return {
...prevfilters,
busLines: false,
wards: {
...prevfilters.wards,
selectedWard: null,
},
};
})
}
>
Clear Routes
</button>
<Filter
filterOpen={filterOpen}
setFilterOpen={setFilterOpen}
currentFilters={currentFilters}
setCurrentFilters={setCurrentFilters}
/>
<Search
onChangeSearch={onChangeSearch}
searchTerm={searchTerm}
searchResultsElements={searchResultsElements}
wardFilter
/>

<TileLayer
@@ -279,6 +214,7 @@ export default function WardMap() {
<GeoJSON
data={wardRankings.features}
onEachFeature={onEachWardFeature}
key={JSON.stringify(currentFilters)}
/>
)}
{currentFilters.busLines && (
7 changes: 7 additions & 0 deletions src/css/main.css
Original file line number Diff line number Diff line change
@@ -601,6 +601,13 @@ header .h1-container .subtitle-container img {
min-width: 250px;
}

.route-close-btn {
position: absolute;
right: 70px;
bottom: 45px;
z-index: 150;
}

.search-container {
position: absolute;
right: 15px;
7 changes: 7 additions & 0 deletions src/scss/_map.scss
Original file line number Diff line number Diff line change
@@ -23,3 +23,10 @@
min-width: 250px;
}
}

.route-close-btn {
position: absolute;
right: 70px;
bottom: 45px;
z-index: 150;
}
7 changes: 3 additions & 4 deletions src/utils/percentileKeys.js
Original file line number Diff line number Diff line change
@@ -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;
}
62 changes: 62 additions & 0 deletions src/utils/routeStyles.js
Original file line number Diff line number Diff line change
@@ -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
);
}