From 4c19868f96103371db28c2f99bcc6ecf01311588 Mon Sep 17 00:00:00 2001 From: Ana Perez Ghiglia Date: Thu, 18 Jun 2020 18:58:00 -0300 Subject: [PATCH] Add map legend (#115) * Add legend to draggable map * round clusters labels limits to the 3-most-significant digits * remove breadcrumbs and my story title from dashboard * Re-arrange dashboard status, suggestions and latestUpdates elements for map legend to fit without overlapping * remove unnecessary line in dev-setup.sh --- backend/requirements.txt | 1 + backend/router/data.py | 20 ++- dev-setup.sh | 1 - frontend/src/components/Map/index.js | 42 +++++- frontend/src/components/Map/styles.module.css | 31 +++++ frontend/src/css/index.css | 5 - frontend/src/routes/Dashboard/index.js | 123 ++++++++++-------- .../src/routes/Dashboard/styles.module.css | 70 ++++++---- 8 files changed, 199 insertions(+), 94 deletions(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 5db4d430..858bbfda 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -12,3 +12,4 @@ python-multipart requests pandas jenkspy +sigfig diff --git a/backend/router/data.py b/backend/router/data.py index 7c1cca3a..18f6093f 100644 --- a/backend/router/data.py +++ b/backend/router/data.py @@ -4,6 +4,7 @@ import pandas as pd import requests from fastapi import APIRouter +from sigfig import round import jenkspy @@ -64,6 +65,8 @@ def cluster_data(confirmed, clusters_config=None): df["confirmed"], nb_class=clusters_config["clusters"] ) + rounded_breaks = list(map(lambda limit: round(limit, sigfigs=3), breaks)) + df["group"] = pd.cut( df["confirmed"], bins=breaks, @@ -71,9 +74,12 @@ def cluster_data(confirmed, clusters_config=None): include_lowest=True, ) df = df.where(pd.notnull(df), None) # convert NaN to None + non_inclusive_lower_limits = list( + map(lambda limit: 0 if limit == 0 else limit + 1, rounded_breaks) + ) # add 1 to all limits (except 0) return { "data": df.to_dict("records"), - "clusters": list(zip(breaks, breaks[1:])), + "clusters": list(zip(non_inclusive_lower_limits, rounded_breaks[1:])), } @@ -93,13 +99,11 @@ def get_covid_us_states_data(): def get_all_data(): countries = fetch_world_data() us_states = fetch_us_states_data() + cluster_labels = [0.2, 0.4, 0.6, 0.8, 1] clustered_data = cluster_data( countries + us_states, - clusters_config={ - "clusters": 10, - "labels": [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1], - }, + clusters_config={"clusters": 5, "labels": cluster_labels}, ) grouped_data = functools.reduce(group_by_scope, clustered_data["data"], {}) @@ -108,7 +112,11 @@ def get_all_data(): grouped_data[DataScope.ADM1] ) - return {"data": grouped_data, "clusters": clustered_data["clusters"]} + return { + "data": grouped_data, + "clusters": clustered_data["clusters"], + "groups": cluster_labels, + } def group_by_scope(base, entry): diff --git a/dev-setup.sh b/dev-setup.sh index f69d810f..cb5b4085 100755 --- a/dev-setup.sh +++ b/dev-setup.sh @@ -7,7 +7,6 @@ pre-commit install -f # lift & update containers docker-compose build -docker-compose up db docker-compose run --rm api pip install -r requirements.txt docker-compose run --rm api pip install -r requirements.dev.txt docker-compose run --rm api python init_db.py diff --git a/frontend/src/components/Map/index.js b/frontend/src/components/Map/index.js index 6cdbab3e..67096886 100644 --- a/frontend/src/components/Map/index.js +++ b/frontend/src/components/Map/index.js @@ -24,6 +24,7 @@ export default function Map({ draggable = true }) { lng: -119.6, lat: 36.7, }); + const [legendRanges, setLegendRanges] = useState([]); useEffect(() => { getUserLocation(); @@ -51,6 +52,19 @@ export default function Map({ draggable = true }) { }); }, [location, map]); + const addLegend = (data) => { + const clusters = data.clusters; + const colorGroups = data.groups; + const newRanges = + clusters && + clusters.map((range, i) => { + return { + label: `${range[0].toLocaleString()} - ${range[1].toLocaleString()}`, + color: getColor(colorGroups[i]), + }; + }); + newRanges && setLegendRanges(newRanges); + }; const getUserLocation = async () => { const userLocation = await fetchUserLocation(); if (userLocation) { @@ -64,8 +78,10 @@ export default function Map({ draggable = true }) { const addLayers = async (map) => { const data = await fetchCovidData(dataScope.ALL); - const worldData = data["adm0"]; - const usStatesData = data["adm1"]["US"]; + const worldData = data["data"]["adm0"]; + const usStatesData = data["data"]["adm1"]["US"]; + + addLegend(data); map.on("load", function () { addWorldLayer(map, worldData); @@ -85,7 +101,7 @@ export default function Map({ draggable = true }) { const body = await api(`data/${scope}`, { method: "GET", }); - return body["data"]; + return body; }; const getColor = (group) => { @@ -214,10 +230,28 @@ export default function Map({ draggable = true }) { ); }; + const legend = ( +
+

Active cases

+ {legendRanges.map((range, i) => ( +
+ + {range.label} +
+ ))} +
+ ); + + const draggableDependantFeatures = () => { + if (draggable) { + return legendRanges.length !== 0 ? legend : null; + } + return
; + }; return (
- {!draggable &&
} + {draggableDependantFeatures()}
); } diff --git a/frontend/src/components/Map/styles.module.css b/frontend/src/components/Map/styles.module.css index 0c9acc64..1b879883 100644 --- a/frontend/src/components/Map/styles.module.css +++ b/frontend/src/components/Map/styles.module.css @@ -20,3 +20,34 @@ .map { z-index: 1; } +.legend { + background-color: rgba(153, 149, 149, 0.6); + border-radius: 3px; + bottom: 30px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + font-size: 12px; + line-height: 20px; + padding: 10px; + position: absolute; + left: 10px; + z-index: 2; + display: block; + text-align: left; +} + +.legend h4 { + margin: 0 0 5px; + color: whitesmoke; +} + +.legend div span { + border-radius: 50%; + display: inline-block; + height: 10px; + margin-right: 5px; + width: 10px; +} + +.legendItem{ + color: whitesmoke; +} diff --git a/frontend/src/css/index.css b/frontend/src/css/index.css index 05d7c4a2..52846408 100644 --- a/frontend/src/css/index.css +++ b/frontend/src/css/index.css @@ -97,11 +97,6 @@ a, a:link, a:visited, a:hover, a:active { left: 5% } -.status-item { - align-items: center; - margin-bottom: 10px; -} - .terms-wrapper { overflow-y: scroll; padding: 1rem 2rem; diff --git a/frontend/src/routes/Dashboard/index.js b/frontend/src/routes/Dashboard/index.js index 6d1b4868..c7b6ff2d 100644 --- a/frontend/src/routes/Dashboard/index.js +++ b/frontend/src/routes/Dashboard/index.js @@ -1,4 +1,3 @@ -import Breadcrumbs from "@material-ui/core/Breadcrumbs"; import Link from "@material-ui/core/Link"; import SpeedDial from "@material-ui/lab/SpeedDial"; import SpeedDialAction from "@material-ui/lab/SpeedDialAction"; @@ -59,63 +58,83 @@ function Dashboard(props) { .then((result) => setData(result)); }, []); + const userStatus = () => ( +
+
+ + {statusMapping[story.sick].name.toUpperCase()} +
+
+ + {statusMapping[story.tested].name.toUpperCase()} +
+
+
+ ); + + const latestUpdate = () => ( + <> +

LATEST TOTALS

+
+
+ ACTIVES +
+ {data.confirmed && data.confirmed.toLocaleString()} +
+
+
+ DEATHS +
+ {data.deaths && data.deaths.toLocaleString()} +
+
+
+ RECOVERED +
+ {data.recovered && data.recovered.toLocaleString()} +
+
+
+ + ); + + const suggestions = () => ( + <> +

SUGGESTIONS

+

Stay at home

+ {getStorySuggestions(story).map((suggestion) => ( + + {suggestion.text} + + ))} + + ); + + const informationHeader = () => ( +
+ {suggestions()} + {userStatus()} + {latestUpdate()} +
+ ); + return (
{status.type === LOADING || !story ? ( status.detail ) : ( <> - -
-

MY STATUS

-
-
- - {statusMapping[story.sick].name} -
-
- - {statusMapping[story.tested].name} -
-
-
-
-
-

LATEST UPDATE

-
-
COVID-19 Cases: {data.confirmed}
-
Total Deaths: {data.deaths}
-
Total Recovered: {data.recovered}
-
-
-
-

SUGGESTIONS

-
Stay at home
- {getStorySuggestions(story).map((suggestion) => ( - - {suggestion.text} - - ))} -
+ {informationHeader()}