Skip to content

Commit

Permalink
Add map legend (#115)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
anaPerezGhiglia authored Jun 18, 2020
1 parent 616897a commit 4c19868
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 94 deletions.
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ python-multipart
requests
pandas
jenkspy
sigfig
20 changes: 14 additions & 6 deletions backend/router/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pandas as pd
import requests
from fastapi import APIRouter
from sigfig import round

import jenkspy

Expand Down Expand Up @@ -64,16 +65,21 @@ 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,
labels=clusters_config["labels"],
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:])),
}


Expand All @@ -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"], {})
Expand All @@ -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):
Expand Down
1 change: 0 additions & 1 deletion dev-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 38 additions & 4 deletions frontend/src/components/Map/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default function Map({ draggable = true }) {
lng: -119.6,
lat: 36.7,
});
const [legendRanges, setLegendRanges] = useState([]);

useEffect(() => {
getUserLocation();
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand All @@ -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) => {
Expand Down Expand Up @@ -214,10 +230,28 @@ export default function Map({ draggable = true }) {
);
};

const legend = (
<div className={classNames(styles.legend)} id="legend">
<h4>Active cases</h4>
{legendRanges.map((range, i) => (
<div className={classNames(styles.legendItem)} key={i}>
<span style={{ backgroundColor: range.color }}></span>
{range.label}
</div>
))}
</div>
);

const draggableDependantFeatures = () => {
if (draggable) {
return legendRanges.length !== 0 ? legend : null;
}
return <div className={classNames(styles.fill, styles.mask)} />;
};
return (
<div className={styles.root}>
<div className={classNames(styles.fill)} id="map"></div>
{!draggable && <div className={classNames(styles.fill, styles.mask)} />}
{draggableDependantFeatures()}
</div>
);
}
31 changes: 31 additions & 0 deletions frontend/src/components/Map/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
5 changes: 0 additions & 5 deletions frontend/src/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
123 changes: 71 additions & 52 deletions frontend/src/routes/Dashboard/index.js
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -59,63 +58,83 @@ function Dashboard(props) {
.then((result) => setData(result));
}, []);

const userStatus = () => (
<div className={classNames(styles.statusList)}>
<div className={classNames("row", styles.statusItem)}>
<span
className={styles.dot}
style={{ background: statusMapping[story.sick].color }}
/>
{statusMapping[story.sick].name.toUpperCase()}
</div>
<div className={classNames("row", styles.statusItem)}>
<span
className={styles.dot}
style={{ background: statusMapping[story.tested].color }}
/>
{statusMapping[story.tested].name.toUpperCase()}
</div>
<div></div>
</div>
);

const latestUpdate = () => (
<>
<h3>LATEST TOTALS</h3>
<div className="row">
<div className={classNames(styles.totalItem)}>
ACTIVES
<div className={classNames(styles.totalItemNum)}>
{data.confirmed && data.confirmed.toLocaleString()}
</div>
</div>
<div className={classNames(styles.totalItem)}>
DEATHS
<div className={classNames(styles.totalItemNum)}>
{data.deaths && data.deaths.toLocaleString()}
</div>
</div>
<div className={classNames(styles.totalItem)}>
RECOVERED
<div className={classNames(styles.totalItemNum)}>
{data.recovered && data.recovered.toLocaleString()}
</div>
</div>
</div>
</>
);

const suggestions = () => (
<>
<h3>SUGGESTIONS</h3>
<p>Stay at home</p>
{getStorySuggestions(story).map((suggestion) => (
<Link
href={suggestion.site}
{...(suggestion.color ? { style: { color: suggestion.color } } : {})}
target="_blank"
>
{suggestion.text}
</Link>
))}
</>
);

const informationHeader = () => (
<div className={classNames(styles.box, styles.top, styles.header)}>
{suggestions()}
{userStatus()}
{latestUpdate()}
</div>
);

return (
<div className={styles.root}>
{status.type === LOADING || !story ? (
status.detail
) : (
<>
<Breadcrumbs aria-label="breadcrumb" className={styles.breadcrumbs} />
<div className={classNames(styles.box, styles.top, styles.left)}>
<h3>MY STATUS</h3>
<div className="status-list">
<div className="row status-item">
<span
className={styles.dot}
style={{ background: statusMapping[story.sick].color }}
/>
{statusMapping[story.sick].name}
</div>
<div className="row status-item">
<span
className={styles.dot}
style={{ background: statusMapping[story.tested].color }}
/>
{statusMapping[story.tested].name}
</div>
<div></div>
</div>
</div>
<div className={classNames(styles.box, styles.top, styles.right)}>
<h3>LATEST UPDATE</h3>
<div>
<div>COVID-19 Cases: {data.confirmed}</div>
<div>Total Deaths: {data.deaths}</div>
<div>Total Recovered: {data.recovered}</div>
</div>
</div>
<div
className={classNames(
styles.box,
styles.bottom,
styles.left,
styles.wrapper
)}
>
<h3>SUGGESTIONS</h3>
<div>Stay at home</div>
{getStorySuggestions(story).map((suggestion) => (
<Link
href={suggestion.site}
{...(suggestion.color
? { style: { color: suggestion.color } }
: {})}
target="_blank"
>
{suggestion.text}
</Link>
))}
</div>
{informationHeader()}
<SpeedDial
ariaLabel="Daily actions"
className={classNames("speeddial", styles.speeddial)}
Expand Down
Loading

0 comments on commit 4c19868

Please sign in to comment.