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 map legend #115

Merged
merged 10 commits into from Jun 18, 2020
1 change: 1 addition & 0 deletions backend/requirements.txt
Expand Up @@ -12,3 +12,4 @@ python-multipart
requests
pandas
jenkspy
sigfig
20 changes: 14 additions & 6 deletions backend/router/data.py
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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100% open to discussion, but, do you really think it's worth it to add another lib to the backend when the front can do this out of the box?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I add it in the back since

  • at first was considering this rounded limits for clustering the data, then realized that it was not a good solution for what i mentioned in the PR description
  • I like the fact that front doesn't make decisions about the data itself, it just formats it in the way it likes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and I don't feel like rounding the data is just format ..


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
Expand Up @@ -7,7 +7,6 @@ pre-commit install -f

# lift & update containers
docker-compose build
docker-compose up db
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ty so much

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
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()}`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See, here's were we could be doing the native rounding I was speaking about before with either this native JS thing I mentioned before, or actually, passing some options like maximumSignificantDigits to the toLocaleString() we're already using!

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
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
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
@@ -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