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

Filter app #104

Merged
merged 17 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/eluc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
pip install pylint
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with PyLint
run: pylint ./*
run: pylint .
- name: Lint with Flake8
run: flake8
- name: Run unit tests
Expand Down
6 changes: 3 additions & 3 deletions use_cases/eluc/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ __pycache__
.DS_Store

.ipynb_checkpoints/
demo.ipynb
data/
predictors/
experiments/
predictors/trained_models/
prescriptors/trained_models/
2 changes: 0 additions & 2 deletions use_cases/eluc/.pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ recursive=y

fail-under=9.7

jobs=0

max-line-length=120

suggestion-mode=yes
Expand Down
2 changes: 1 addition & 1 deletion use_cases/eluc/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
# Copy source files over
COPY . .

# Python setup script - downloads data and processes it
# Download data
RUN python -m app.process_data

# Expose Flask (Dash) port
Expand Down
14 changes: 12 additions & 2 deletions use_cases/eluc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,15 +198,25 @@ TODO: see task #49

A user interface for decision makers is available here: https://landuse.evolution.ml/

See [demo/README.md](demo/README.md)
To run the app, first download the preprocessed dataset from [HuggingFace](https://huggingface.co/datasets/projectresilience/land-use-app-data) using ```python -m app.process_data```.

Then run the app using ```python -m app.app```.

In order to build and run a docker image containing the app, first build with

```docker build -t landusedemo .```

and then run with

```docker run -p 8080:4057 --name landuse-demo-container landusedemo```.

## Testing

To run unit tests, use the following command: ```python -m unittest```.

TODO: see task #79

To run pylint, use the following command: ```pylint ./*```
To run pylint, use the following command: ```pylint .```

## References

Expand Down
127 changes: 29 additions & 98 deletions use_cases/eluc/app/app.py
Original file line number Diff line number Diff line change
@@ -1,118 +1,49 @@
"""
Main app file for ELUC demo.
Uses many 'components' to separate divs and their related callbacks.
They aren't necessarily truly reusable components, but they help to organize the code.
Main entrypoint to run the app. Contains the layout of the app and registers all the callbacks of each component.
"""
import pandas as pd
from dash import Dash
from dash import dcc
from dash import html
from dash import Dash, html
import dash_bootstrap_components as dbc
import pandas as pd

import app.constants as app_constants
from app.components.chart import ChartComponent
from app.components.legend import LegendComponent
from app.components.lock import LockComponent
from app.components.map import MapComponent
from app.components.prediction import PredictionComponent
from app.components.prescription import PrescriptionComponent
from app.components.intro import IntroComponent
from app.components.context.context import ContextComponent
from app.components.filter import FilterComponent
from app.components.dms.dms import DMSComponent
from app.components.references import ReferencesComponent
from app.components.sliders import SlidersComponent
from app.components.trivia import TriviaComponent
from app.utils import EvolutionHandler
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We get rid of all the old components but reuse some in our new components. The components now correspond to portions of the page rather than by functionality which makes things neater



app = Dash(__name__,
external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.icons.BOOTSTRAP],
prevent_initial_callbacks="initial_duplicate")
server = app.server

df = pd.read_csv(app_constants.DATA_FILE_PATH, index_col=app_constants.INDEX_COLS)

legend_component = LegendComponent()

map_component = MapComponent(df)
map_component.register_update_map_callback(app)
map_component.register_click_map_callback(app)
map_component.register_select_country_callback(app)

prescription_component = PrescriptionComponent(df)
prescription_component.register_select_prescriptor_callback(app)
prescription_component.register_toggle_modal_callback(app)

sliders_component = SlidersComponent(df)
sliders_component.register_set_frozen_reset_sliders_callback(app)
sliders_component.register_show_slider_value_callback(app)
sliders_component.register_sum_to_one_callback(app)

lock_component = LockComponent()

chart_component = ChartComponent(df)
chart_component.register_update_context_chart_callback(app)
chart_component.register_update_presc_chart_callback(app)
app.title = 'Land Use Optimization'

prediction_component = PredictionComponent(df)
prediction_component.register_predictor_callback(app)
prediction_component.register_land_use_callback(app)
app_df = pd.read_csv("app/data/app_data.csv", index_col=app_constants.INDEX_COLS)

trivia_component = TriviaComponent(df)
trivia_component.register_update_trivia_callback(app)
evolution_handler = EvolutionHandler()

intro_component = IntroComponent()
context_component = ContextComponent(app_df, evolution_handler)
filter_component = FilterComponent(evolution_handler)
dms_component = DMSComponent(app_df, evolution_handler)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We pass our app df and evolution handler to the components so that they have the ability to prescribe, look at context, etc.

references_component = ReferencesComponent()

app.title = 'Land Use Optimization'
app.css.config.serve_locally = False
# Don't be afraid of the 3rd party URLs: chriddyp is the author of Dash!
# These two allow us to dim the screen while loading.
# See discussion with Dash devs here: https://community.plotly.com/t/dash-loading-states/5687
app.css.append_css({'external_url': 'https://codepen.io/chriddyp/pen/bWLwgP.css'})
app.css.append_css({'external_url': 'https://codepen.io/chriddyp/pen/brPBPO.css'})

app.layout = html.Div([
dcc.Markdown('''
# Land Use Optimization
This site is for demonstration purposes only.

For a given context cell representing a portion of the earth,
identified by its latitude and longitude coordinates, and a given year:
* What changes can we make to the land usage
* In order to minimize the resulting estimated CO2 emissions? (Emissions from Land Use Change, ELUC,
in tons of carbon per hectare)
context_component.register_callbacks(app)
filter_component.register_callbacks(app)
dms_component.register_callbacks(app)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Instead of all the long series of callbacks we now register all the callbacks at once with these simple functions. We could abstract these into a general Component class later and just call register_callbacks on them all.


'''),
dcc.Markdown('''## Context'''),
html.Div([
dcc.Graph(id="map", figure=map_component.get_map_fig(), style={"grid-column": "1"}),
html.Div([map_component.get_context_div()], style={"grid-column": "2"}),
html.Div([legend_component.get_legend_div()], style={"grid-column": "3"})
], style={"display": "grid", "grid-template-columns": "auto 1fr auto", 'position': 'relative'}),
dcc.Markdown('''## Actions'''),
html.Div([
html.Div([prescription_component.get_presc_select_div()], style={"grid-column": "1"}),
html.Div([chart_component.get_chart_select_div()],
style={"grid-column": "2", "margin-top": "-10px", "margin-left": "10px"}),
], style={"display": "grid", "grid-template-columns": "45% 15%"}),
html.Div([
html.Div(lock_component.get_checklist_div(), style={"grid-column": "1", "height": "100%"}),
html.Div(sliders_component.get_sliders_div(), style={'grid-column': '2'}),
dcc.Graph(id='context-fig',
figure=chart_component.create_treemap(type_context=True),
style={'grid-column': '3'}),
dcc.Graph(id='presc-fig',
figure=chart_component.create_treemap(type_context=False),
style={'grid-clumn': '4'})
], style={'display': 'grid', 'grid-template-columns': '4.5% 40% 1fr 1fr', "width": "100%"}),
# The above line can't be set to auto because the lines will overflow!
html.Div([
sliders_component.get_frozen_div(),
html.Button("Sum to 100%", id='sum-button', n_clicks=0),
html.Div(id='sum-warning')
]),
dcc.Markdown('''## Outcomes'''),
prediction_component.get_predict_div(),
dcc.Markdown('''## Trivia'''),
trivia_component.get_trivia_div(),
dcc.Markdown('''## References'''),
references_component.get_references_div()
], style={'padding-left': '10px'},)
app.layout = html.Div(
children=[
intro_component.get_div(),
context_component.get_div(),
filter_component.get_div(),
dms_component.get_div(),
references_component.get_references_div()
]
)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Layout is now super clean. Just calls get_div from each of our components in the order they should display on the apge


if __name__ == '__main__':
app.run_server(host='0.0.0.0', debug=False, port=4057, use_reloader=False, threaded=False)
app.run_server(host='0.0.0.0', debug=False, port=4057, use_reloader=True, threaded=True)
Copy link
Member

Choose a reason for hiding this comment

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

Did you mean to leave user_reloader to True?

Copy link
Member

Choose a reason for hiding this comment

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

According to https://dash.plotly.com/devtools you're not changing the code in the deployed app, and it makes it slower to start, so I'll leave it to False

6 changes: 6 additions & 0 deletions use_cases/eluc/app/assets/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* body {
background-image: url("https://upload.wikimedia.org/wikipedia/commons/thumb/1/1f/Icelandic_Landscape_near_Neskaupsta%C3%B0ur_July_2014.JPG/640px-Icelandic_Landscape_near_Neskaupsta%C3%B0ur_July_2014.JPG");
background-size: cover;
background-position: center;
background-attachment: fixed;
} */
4 changes: 4 additions & 0 deletions use_cases/eluc/app/assets/tooltip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
window.dccFunctions = window.dccFunctions || {};
window.dccFunctions.percentSlider = function(value) {
return Math.round(value * 100);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So that our sliders show as percents and not proportions (10% vs. 0.1)

127 changes: 127 additions & 0 deletions use_cases/eluc/app/components/context/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""
Context component class for selecting context.
"""
from dash import html, dcc, Output, Input
import dash_bootstrap_components as dbc
import pandas as pd
import regionmask

from app.components.context.map import MapComponent
from app.utils import EvolutionHandler
from data import constants


class ContextComponent():
"""
Component containing map as well as dropdowns and input fields for picking a more specific context.
"""
def __init__(self, app_df: pd.DataFrame, handler: EvolutionHandler):
self.map_component = MapComponent(app_df)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We reuse the old map component in our new context component

self.app_df = app_df
self.handler = handler
self.countries_df = regionmask.defined_regions.natural_earth_v5_0_0.countries_110.to_dataframe()

def create_label_and_value(self, label: str, value: html.Div) -> html.Div:
"""
Standard dash function that pairs a label with any arbitrary value Div.
"""
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This function lines up a label with a div so we reuse it for the Lat/dropdown, lon/dropdown, year/input

div = html.Div(
className="d-flex flex-row",
children=[
html.Label(label, className="w-25"),
html.Div(
value,
className="flex-grow-1"
)
]
)
return div

def get_div(self):
"""
Returns the entire context div to display in the app.
"""
div = html.Div(
className="mb-5 mx-5",
children=[
dbc.Row(
children=[
dbc.Col(
width={"offset": 3, "size": 3},
children=[
dcc.Graph(id="map", figure=self.map_component.get_map_fig()),
dcc.Dropdown(
id="loc-dropdown",
options=list(self.map_component.countries_df["names"]),
value=list(self.map_component.countries_df["names"])[143]
)
]
),
dbc.Col(
width=3,
children=[
html.B("1. Select a land area on the map to optimize or manually enter coordinates."),
self.create_label_and_value(
"Latitude",
dcc.Dropdown(
id="lat-dropdown",
options=[{"label": lat,
"value": lat} for lat in self.map_component.lat_list],
value=51.625,
)
),
self.create_label_and_value(
"Longitude",
dcc.Dropdown(
id="lon-dropdown",
options=[{"label": lon,
"value": lon} for lon in self.map_component.lon_list],
value=-3.375,
)
),
self.create_label_and_value(
"Year",
html.Div([
dcc.Input(
id="year-input",
type="number",
value=2021,
debounce=True
),
dcc.Tooltip(f"Year must be between \
{self.map_component.min_time} and \
{self.map_component.max_time}.")
])
)
]
)
]
)
]
)
return div

def register_callbacks(self, app):
"""
Registers callbacks to make app interactive. Registers old map callbacks as well as new one to run prescription.
"""
self.map_component.register_click_map_callback(app)
self.map_component.register_select_country_callback(app)
self.map_component.register_update_map_callback(app)

@app.callback(
Output("results-store", "data"),
Input("year-input", "value"),
Input("lat-dropdown", "value"),
Input("lon-dropdown", "value")
)
def run_prescription(year: int, lat: float, lon: float) -> dict[str: list]:
"""
Runs prescription for the selected context on all prescriptors. Returns the results as a json to a store.
"""
condition = (self.app_df["time"] == year) & (self.app_df["lat"] == lat) & (self.app_df["lon"] == lon)
context_df = self.app_df[condition]
context_df = context_df[constants.CAO_MAPPING["context"]].iloc[0:1]
results_df = self.handler.prescribe_all(context_df)
results_json = results_df.to_dict(orient="records")
return results_json
Loading
Loading