Skip to content

Commit

Permalink
Merge pull request #104 from Project-Resilience/filter-app
Browse files Browse the repository at this point in the history
Filter app
  • Loading branch information
danyoungday authored Oct 8, 2024
2 parents 216a019 + d6afa2d commit 95f48ac
Show file tree
Hide file tree
Showing 134 changed files with 1,177 additions and 907 deletions.
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


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

'''),
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()
]
)

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=False, threaded=True)
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);
}
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)
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.
"""
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

0 comments on commit 95f48ac

Please sign in to comment.