Skip to content

Commit ca0431d

Browse files
Drew Resnickdrewmresnickremicousin
authored
Crop suitability (#280)
* linking files, and copying some from onset maproom (as templates) * editing the layout and maproom to run an as a standalone app * fix controls symlink * fix assets symlink * add pingrid symlink * starting to adapt the onset maproom to work for the crop suitability maproom * adding a dropdown and some minor edits * fix mistake in checking out old commit * trying to fix mistake in editing a file * pushing most recent changes / additions trying to get tile generation to work * add seasonal means time series plot * getting the tile bug fixed and starting to play around with a function for crop suitability * make tile print output of suitability analysis; create button for user to submit analysis params * add title callback; try to add final parameters for crop suitability analysis * remove empty paragraph inputs * improve readibility of function call * fix rainbow colorscale call from pingrid * edit files to build using path defs in config files * change opacity of colorbar to 1 * remove unused rount_latLng func * remove second .sum() where it is not needed * remove unneeded outputs from callback * fix reading of file paths * move default values for input params to country-wise config file just for nma * remove dry spell inputs for now * remove version of func for selecting season of data that was not used * update suitability index from % to int; update output plot to be bar graph * update maproom title * add option to select different data layers to display * first go at selecting subset of data before calling the crop_suit function * update config options for APP.run_server() * fix pingrid.tile bug; fix error in run_server for defining debug * fixing crop suitability func to handle dataArrays as input when a single lat/lng is selected * add more complex logic from app.py * making crop suitability function create new empty dataset; make inputs for the function all datasets before passing * extract DataArray when loading in data; make change to config var names * move non country specific config entries to config-defaults * update documentation in layout section * update title and description of layers selection ropdown * change id name for submit coords * update map target year title and remove target year from ts callback * fix season and year controls to update on input for map title, ts, and tile * make def of units more compact * update title for submit button * change use of range to amplitude for calc of temperature range * change input title for seasonal rainfall input * add html_size option to Numbers() to control size of numbers input boxes * change def of wet_day_def; add comments for param defs * keep naming convention for time to be T * remove erroneous text * update descriptions of variables * comment out DB configuration for development env * remove unused controls * improve var name and delete unused var * edit time series title * hard code to add coordinates units for this maproom * update map title to reflect Malaria maproom * setting up so it builds like the other maprooms * this is now its own repo * builds with the others and shows a plausible landing page * working precip layer * working suitability index is now default * typo * outputs min. Probably unncessary but will get to colorscales later * correcting a likely oversight of rebasing * changed to dummy initial value for position and commented about it * fill false is enough * empty tiles can be checked for and returned as soon as there is data * more concise crop_suitability function * further simplification: no need for where to make 1s and 0s (but does need 1*) * earlier tiling * no need for compute * crop suitability function should get the data type it should get * more concise way to set up the xr.Dataset * more proper place to convert the bool to int --------- Co-authored-by: drewmresnick <[email protected]> Co-authored-by: remic <[email protected]>
1 parent 26e9f10 commit ca0431d

File tree

5 files changed

+953
-0
lines changed

5 files changed

+953
-0
lines changed

enacts/config-defaults.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,32 @@ maprooms:
112112
# description: The map shows the crop evapotranspiration...
113113
# units: mm
114114

115+
#crop suitability
116+
crop_suitability:
117+
118+
# App
119+
core_path: /crops_climate_suitability
120+
121+
app_title: Climate Suitability for Crops
122+
crop_suit_title: Climate Suitability for Crops
123+
title: Climate Suitability for Crops
124+
layers:
125+
suitability_layer:
126+
menu_label: Crop climate suitability
127+
description: The map shows the count of suitable climate conditions for a specific crop.
128+
units: integer index
129+
id: suit
130+
map_max: 5
131+
precip_layer:
132+
menu_label: Precipitation
133+
description: Seasonal Precipitation Total.
134+
units: mm
135+
tmax_layer:
136+
menu_label: Maximum temperature
137+
description: Seasonal Maximum Temperature.
138+
units: C
139+
tmin_layer:
140+
menu_label: Minimum temperature
141+
description: Seasonal Minimum Temperature.
142+
units: C
143+

enacts/crop_suitability/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import maproom_crop_suit

enacts/crop_suitability/assets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../assets/
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
import os
2+
from dash import dcc
3+
from dash import html
4+
import dash_bootstrap_components as dbc
5+
from dash import dash_table
6+
import dash_leaflet as dlf
7+
import plotly.express as px
8+
from controls import Block, Sentence, DateNoYear, Number
9+
10+
import numpy as np
11+
from pathlib import Path
12+
import calc
13+
import pingrid
14+
import pandas as pd
15+
16+
from globals_ import GLOBAL_CONFIG
17+
18+
CONFIG = GLOBAL_CONFIG["maprooms"]["crop_suitability"]
19+
20+
DR_PATH = f'{GLOBAL_CONFIG["datasets"]["daily"]["zarr_path"]}{GLOBAL_CONFIG["datasets"]["daily"]["vars"]["precip"][1]}'
21+
RR_MRG_ZARR = Path(DR_PATH)
22+
23+
IRI_BLUE = "rgb(25,57,138)"
24+
IRI_GRAY = "rgb(113,112,116)"
25+
LIGHT_GRAY = "#eeeeee"
26+
27+
28+
def app_layout():
29+
30+
# Initialization
31+
rr_mrg = calc.read_zarr_data(RR_MRG_ZARR)
32+
center_of_the_map = [((rr_mrg["Y"][int(rr_mrg["Y"].size/2)].values)), ((rr_mrg["X"][int(rr_mrg["X"].size/2)].values))]
33+
lat_res = np.around((rr_mrg["Y"][1]-rr_mrg["Y"][0]).values, decimals=10)
34+
lat_min = np.around((rr_mrg["Y"][0]-lat_res/2).values, decimals=10)
35+
lat_max = np.around((rr_mrg["Y"][-1]+lat_res/2).values, decimals=10)
36+
lon_res = np.around((rr_mrg["X"][1]-rr_mrg["X"][0]).values, decimals=10)
37+
lon_min = np.around((rr_mrg["X"][0]-lon_res/2).values, decimals=10)
38+
lon_max = np.around((rr_mrg["X"][-1]+lon_res/2).values, decimals=10)
39+
lat_label = str(lat_min)+" to "+str(lat_max)+" by "+str(lat_res)+"˚"
40+
lon_label = str(lon_min)+" to "+str(lon_max)+" by "+str(lon_res)+"˚"
41+
42+
return dbc.Container(
43+
[
44+
dcc.Location(id="location", refresh=True),
45+
navbar_layout(),
46+
dbc.Row(
47+
[
48+
dbc.Col(
49+
controls_layout(lat_min, lat_max, lon_min, lon_max, lat_label, lon_label),
50+
sm=12,
51+
md=4,
52+
style={
53+
"background-color": "white",
54+
"border-style": "solid",
55+
"border-color": LIGHT_GRAY,
56+
"border-width": "0px 1px 0px 0px",
57+
"overflow":"scroll","height":"95vh",#column that holds text and controls
58+
},
59+
),
60+
dbc.Col(
61+
[
62+
dbc.Row(
63+
[
64+
dbc.Col(
65+
map_layout(center_of_the_map, lon_min, lat_min, lon_max, lat_max),
66+
width=12,
67+
style={
68+
"background-color": "white",
69+
},
70+
),
71+
],
72+
style={"overflow":"scroll","height": "55%"}, #box the map is in
73+
className="g-0",
74+
),
75+
dbc.Row(
76+
[
77+
dbc.Col(
78+
results_layout(),
79+
width=12,
80+
style={
81+
"background-color": "white",
82+
"min-height": "100px",
83+
"border-style": "solid",
84+
"border-color": LIGHT_GRAY,
85+
"border-width": "1px 0px 0px 0px",
86+
},
87+
),
88+
],
89+
style={"overflow":"scroll","height":"45%"}, #box the plots are in
90+
className="g-0",
91+
),
92+
],style={"overflow":"scroll","height":"95vh"},#main column for map and results
93+
sm=12,
94+
md=8,
95+
),
96+
],
97+
className="g-0",
98+
),
99+
],
100+
fluid=True,
101+
style={"padding-left": "1px", "padding-right": "1px","height":"100%"},
102+
)
103+
104+
105+
def navbar_layout():
106+
return dbc.Navbar(
107+
[
108+
html.A(
109+
dbc.Row(
110+
[
111+
dbc.Col(
112+
html.Img(
113+
src="assets/" + GLOBAL_CONFIG["logo"],
114+
height="30px",
115+
)
116+
),
117+
dbc.Col(
118+
dbc.NavbarBrand(
119+
"Climate and Agriculture / " + CONFIG["crop_suit_title"],
120+
className="ml-2",
121+
)
122+
),
123+
],
124+
align="center",
125+
className="g-0",
126+
),
127+
),
128+
dbc.NavbarToggler(id="navbar-toggler"),
129+
dbc.Collapse(
130+
id="navbar-collapse",
131+
navbar=True,
132+
),
133+
],
134+
sticky="top",
135+
color=IRI_GRAY,
136+
dark=True,
137+
)
138+
139+
140+
def controls_layout(lat_min, lat_max, lon_min, lon_max, lat_label, lon_label):
141+
return dbc.Container(
142+
[
143+
html.Div(
144+
[
145+
html.H5(CONFIG["crop_suit_title"]),
146+
html.P(
147+
[f"""
148+
Maproom to explore crop climate suitability using a
149+
spatial overlay analysis. The output is an index from
150+
0-5
151+
where 5 meet all conditions and is the most suitable.
152+
""",
153+
html.Br(),
154+
"""Select from the layers dropdown to select which data
155+
to view as timeseries data and as a map layer. You can
156+
Select a custom point to view for the timeseries data,
157+
as well as a specific year to view on the map.""",
158+
html.Br(),
159+
"""Select custom values to determine the crop suitability
160+
parameters passed into the analysis to determine the
161+
most suitable conditions for your inquiry.""",
162+
]
163+
)
164+
]+[
165+
html.H5("Dataset Documentation"),
166+
html.P(
167+
f"""
168+
Reconstructed gridded rainfall, minimum temperature, and maximum temperature from {GLOBAL_CONFIG["institution"]}.
169+
The time series data were created by combining
170+
quality-controlled station observations in
171+
{GLOBAL_CONFIG["institution"]}’s archive with satellite rainfall estimates.
172+
Crop suitability values use the rainfall, mimumum and maximum temperature data as inputs to calculate a suitability index where a certain number of conditions of crop suitability are met.
173+
"""
174+
),
175+
]+[
176+
html.P([html.H6(val["menu_label"]), html.P(val["description"])])
177+
for key, val in CONFIG["layers"].items()
178+
],
179+
style={"position":"relative","height":"25%", "overflow":"scroll"},#box holding text
180+
),
181+
html.H3("Controls Panel",style={"padding":".5rem"}),
182+
html.Div(
183+
[
184+
Block("Data Layers",
185+
"Choose a data layer to view:",
186+
dbc.Select(
187+
id="data_choice",
188+
value=list(CONFIG["layers"].keys())[0],
189+
options=[
190+
{"label": val["menu_label"], "value": key}
191+
for key, val in CONFIG["layers"].items()
192+
],
193+
),
194+
),
195+
Block("Pick a point",
196+
dbc.Row(
197+
[
198+
dbc.Col(
199+
dbc.FormFloating([dbc.Input(
200+
id = "lat_input",
201+
min=lat_min,
202+
max=lat_max,
203+
type="number",
204+
),
205+
dbc.Label("Latitude", style={"font-size": "80%"}),
206+
dbc.Tooltip(
207+
f"{lat_label}",
208+
target="lat_input",
209+
className="tooltiptext",
210+
)]),
211+
),
212+
dbc.Col(
213+
dbc.FormFloating([dbc.Input(
214+
id = "lng_input",
215+
min=lon_min,
216+
max=lon_max,
217+
type="number",
218+
),
219+
dbc.Label("Longitude", style={"font-size": "80%"}),
220+
dbc.Tooltip(
221+
f"{lon_label}",
222+
target="lng_input",
223+
className="tooltiptext",
224+
)]),
225+
),
226+
dbc.Button(id="submit_coords", outline=True, color="primary", children='Submit lat lng'),
227+
],
228+
),
229+
"Map year:",
230+
dbc.Input(
231+
id = "target_year",
232+
type = "number",
233+
min = 1961,
234+
max = 2018,
235+
value = CONFIG["param_defaults"]["target_year"],
236+
),
237+
"Season of interest:",
238+
dbc.Select(
239+
id="target_season",
240+
value= CONFIG["param_defaults"]["target_season"],
241+
options=[
242+
{"label":"Dec-Feb", "value":"DJF"},
243+
{"label":"Mar-May", "value":"MAM"},
244+
{"label":"Jun-Aug", "value":"JJA"},
245+
{"label":"Sep-Nov", "value":"SON"},
246+
],
247+
),
248+
),
249+
Block(
250+
"Optimum seasonal total rainfall",
251+
Sentence(
252+
"Total rainfall amount between",
253+
Number("lower_wet_threshold", CONFIG["param_defaults"]["lower_wet_thresh"], min=0, max=99999, html_size=4),
254+
"and",
255+
Number("upper_wet_threshold", CONFIG["param_defaults"]["upper_wet_thresh"], min=0, max=99999, html_size=4),
256+
"mm",
257+
),
258+
),
259+
Block(
260+
"Temperature tolerance",
261+
Sentence(
262+
"Temperature range between",
263+
Number("minimum_temp", CONFIG["param_defaults"]["min_temp"], min=-99, max=999, html_size=2),
264+
"and",
265+
Number("maximum_temp", CONFIG["param_defaults"]["max_temp"], min=-99, max=99999, html_size=2),
266+
"C",
267+
),
268+
),
269+
Block(
270+
"Optimal daily temperature amplitude",
271+
Sentence(
272+
"An average daily temperature amplitude of:",
273+
Number("temp_range", CONFIG["param_defaults"]["temp_range"], min=0, max=99999, html_size=2),
274+
"C",
275+
),
276+
),
277+
# Block(
278+
# "Season length",
279+
# Sentence(
280+
# "The minimum length of the season:",
281+
# Number("season_length", CONFIG["param_defaults"]["season_length"], min=0, max=99999, html_size=3),
282+
# "days",
283+
# ),
284+
# ),
285+
Block(
286+
"Wet days",
287+
Sentence(
288+
"The minimum number of wet days within a season:",
289+
Number("min_wet_days", CONFIG["param_defaults"]["min_wet_days"], min=0, max=99999, html_size=3),
290+
"days",
291+
),
292+
Sentence(
293+
"Where a wet day is defined as a day with rainfall more than:",
294+
Number("wet_day_def", CONFIG["param_defaults"]["wet_day_def"], min=0, max=9999, html_size=3),
295+
"mm",
296+
),
297+
),
298+
],
299+
style={"position":"relative","height":"60%", "overflow":"scroll"},#box holding controls
300+
),
301+
html.Div(
302+
[dbc.Button(id="submit_params", children='Submit Crop Suitability Conditions', style={"position":"fixed", "width":"31%"})],
303+
)
304+
], #end container
305+
fluid=True,
306+
className="scrollable-panel p-3",
307+
style={"overflow":"scroll","height":"100%","padding-bottom": "1rem", "padding-top": "1rem"},
308+
) #style for container that is returned #95vh
309+
310+
def map_layout(center_of_the_map, lon_min, lat_min, lon_max, lat_max):
311+
return dbc.Container(
312+
[
313+
dlf.Map(
314+
[
315+
dlf.LayersControl(id="layers_control", position="topleft"),
316+
dlf.LayerGroup(
317+
#Dummy position overridden by pick_location initial callback
318+
#position must be defined
319+
[dlf.Marker(id="loc_marker", position=[0, 0])],
320+
id="layers_group"
321+
),
322+
dlf.ScaleControl(imperial=False, position="topright"),
323+
dlf.Colorbar(
324+
id="colorbar",
325+
position="bottomleft",
326+
width=300,
327+
height=10,
328+
opacity=1,
329+
)
330+
],
331+
id="map",
332+
center=center_of_the_map,
333+
zoom=GLOBAL_CONFIG["zoom"],
334+
maxBounds = [[lat_min, lon_min],[lat_max, lon_max]],
335+
minZoom = GLOBAL_CONFIG["zoom"] - 1,
336+
maxZoom = GLOBAL_CONFIG["zoom"] + 10, #this was completely arbitrary
337+
style={
338+
"width": "100%",
339+
"height": "87%",#height of the map
340+
},
341+
),
342+
html.H6(
343+
id="map_title"
344+
),
345+
html.H6(
346+
id="hover_feature_label"
347+
)
348+
],
349+
fluid=True,
350+
style={"padding": "0rem", "height":"100%"},#box that holds map and title
351+
)
352+
353+
354+
def results_layout():
355+
return html.Div(
356+
[
357+
dbc.Spinner(dcc.Graph(id="timeseries_graph")),
358+
],
359+
id="results_div",
360+
)

0 commit comments

Comments
 (0)