Spaces:
Running
Running
Commit
·
38804a1
1
Parent(s):
b834a5d
wip
Browse filesoptimized functions. Still need to figure out why removing unknown status makes the stacked bar charts go wonky...
- app/app.py +8 -8
- app/utils.py +321 -442
app/app.py
CHANGED
@@ -195,7 +195,7 @@ def run_sql(query,color_choice):
|
|
195 |
|
196 |
elif ("id" and "geom" in result.columns):
|
197 |
style = get_pmtiles_style_llm(style_options[color_choice], result["id"].tolist())
|
198 |
-
legend, position, bg_color, fontsize =
|
199 |
|
200 |
m.add_legend(legend_dict = legend, position = position, bg_color = bg_color, fontsize = fontsize)
|
201 |
m.add_pmtiles(ca_pmtiles, style=style, opacity=alpha, tooltip=True, fit_bounds=True)
|
@@ -229,7 +229,7 @@ with st.sidebar:
|
|
229 |
|
230 |
st.divider()
|
231 |
color_choice = st.radio("Group by:", style_options, key = "color", help = "Select a category to change map colors and chart groupings.")
|
232 |
-
colorby_vals =
|
233 |
alpha = 0.8
|
234 |
st.divider()
|
235 |
|
@@ -348,9 +348,9 @@ with st.sidebar:
|
|
348 |
for label in style_options: # get selected filters (based on the buttons selected)
|
349 |
with st.expander(label):
|
350 |
if label in ["GAP Code","30x30 Status"]: # gap code 1 and 2 are on by default
|
351 |
-
opts =
|
352 |
else: # other buttons are not on by default.
|
353 |
-
opts =
|
354 |
filters.update(opts)
|
355 |
|
356 |
selected = {k: v for k, v in filters.items() if v}
|
@@ -371,7 +371,7 @@ with st.sidebar:
|
|
371 |
# Display CA 30x30 Data
|
372 |
if 'out' not in locals():
|
373 |
style = get_pmtiles_style(style_options[color_choice], alpha, filter_cols, filter_vals)
|
374 |
-
legend, position, bg_color, fontsize =
|
375 |
m.add_legend(legend_dict = legend, position = position, bg_color = bg_color, fontsize = fontsize)
|
376 |
m.add_pmtiles(ca_pmtiles, style=style, name="CA", opacity=alpha, tooltip=True, fit_bounds=True)
|
377 |
|
@@ -397,11 +397,11 @@ colors = (
|
|
397 |
# get summary tables used for charts + printed table
|
398 |
# df - charts; df_tab - printed table (omits colors)
|
399 |
if 'out' not in locals():
|
400 |
-
df, df_tab, df_percent, df_bar_30x30 =
|
401 |
total_percent = 100*df_percent.percent_CA.sum()
|
402 |
|
403 |
else:
|
404 |
-
df =
|
405 |
total_percent = 100*df.percent_CA.sum()
|
406 |
|
407 |
|
@@ -431,7 +431,7 @@ with main:
|
|
431 |
with st.container():
|
432 |
|
433 |
st.markdown(f"{total_percent}% CA Protected", help = "Total percentage of 30x30 conserved lands, updates based on displayed data")
|
434 |
-
st.altair_chart(
|
435 |
|
436 |
if 'df_bar_30x30' in locals(): #if we use chatbot, we won't have these graphs.
|
437 |
if column not in ["status", "gap_code"]:
|
|
|
195 |
|
196 |
elif ("id" and "geom" in result.columns):
|
197 |
style = get_pmtiles_style_llm(style_options[color_choice], result["id"].tolist())
|
198 |
+
legend, position, bg_color, fontsize = get_legend(style_options,color_choice)
|
199 |
|
200 |
m.add_legend(legend_dict = legend, position = position, bg_color = bg_color, fontsize = fontsize)
|
201 |
m.add_pmtiles(ca_pmtiles, style=style, opacity=alpha, tooltip=True, fit_bounds=True)
|
|
|
229 |
|
230 |
st.divider()
|
231 |
color_choice = st.radio("Group by:", style_options, key = "color", help = "Select a category to change map colors and chart groupings.")
|
232 |
+
colorby_vals = get_color_vals(style_options, color_choice) #get options for selected color_by column
|
233 |
alpha = 0.8
|
234 |
st.divider()
|
235 |
|
|
|
348 |
for label in style_options: # get selected filters (based on the buttons selected)
|
349 |
with st.expander(label):
|
350 |
if label in ["GAP Code","30x30 Status"]: # gap code 1 and 2 are on by default
|
351 |
+
opts = get_buttons(style_options, label, default_boxes)
|
352 |
else: # other buttons are not on by default.
|
353 |
+
opts = get_buttons(style_options, label)
|
354 |
filters.update(opts)
|
355 |
|
356 |
selected = {k: v for k, v in filters.items() if v}
|
|
|
371 |
# Display CA 30x30 Data
|
372 |
if 'out' not in locals():
|
373 |
style = get_pmtiles_style(style_options[color_choice], alpha, filter_cols, filter_vals)
|
374 |
+
legend, position, bg_color, fontsize = get_legend(style_options, color_choice)
|
375 |
m.add_legend(legend_dict = legend, position = position, bg_color = bg_color, fontsize = fontsize)
|
376 |
m.add_pmtiles(ca_pmtiles, style=style, name="CA", opacity=alpha, tooltip=True, fit_bounds=True)
|
377 |
|
|
|
397 |
# get summary tables used for charts + printed table
|
398 |
# df - charts; df_tab - printed table (omits colors)
|
399 |
if 'out' not in locals():
|
400 |
+
df, df_tab, df_percent, df_bar_30x30 = get_summary_table(ca, column, select_colors, color_choice, filter_cols, filter_vals,colorby_vals)
|
401 |
total_percent = 100*df_percent.percent_CA.sum()
|
402 |
|
403 |
else:
|
404 |
+
df = get_summary_table_sql(ca, column, colors, ids)
|
405 |
total_percent = 100*df.percent_CA.sum()
|
406 |
|
407 |
|
|
|
431 |
with st.container():
|
432 |
|
433 |
st.markdown(f"{total_percent}% CA Protected", help = "Total percentage of 30x30 conserved lands, updates based on displayed data")
|
434 |
+
st.altair_chart(area_chart(df, column), use_container_width=True)
|
435 |
|
436 |
if 'df_bar_30x30' in locals(): #if we use chatbot, we won't have these graphs.
|
437 |
if column not in ["status", "gap_code"]:
|
app/utils.py
CHANGED
@@ -1,400 +1,45 @@
|
|
1 |
import streamlit as st
|
2 |
import streamlit.components.v1 as components
|
3 |
-
import
|
4 |
import leafmap.maplibregl as leafmap
|
5 |
import altair as alt
|
6 |
import ibis
|
7 |
from ibis import _
|
8 |
import ibis.selectors as s
|
9 |
import os
|
10 |
-
import pandas as pd
|
11 |
-
import geopandas as gpd
|
12 |
from shapely import wkb
|
13 |
-
import sqlalchemy
|
14 |
-
import pathlib
|
15 |
from typing import Optional
|
16 |
from functools import reduce
|
17 |
from itertools import chain
|
18 |
|
19 |
from variables import *
|
20 |
|
21 |
-
def colorTable(select_colors,color_choice,column):
|
22 |
-
colors = (ibis
|
23 |
-
.memtable(select_colors[color_choice], columns=[column, "color"])
|
24 |
-
.to_pandas()
|
25 |
-
)
|
26 |
-
return colors
|
27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
group_totals = df.group_by(main_group).aggregate(total_acres=_.acres.sum())
|
36 |
-
df = ca.filter(combined_filter)
|
37 |
-
df = (df
|
38 |
-
.group_by(*column) # unpack the list for grouping
|
39 |
-
.aggregate(percent_CA= _.acres.sum() / ca_area_acres,
|
40 |
-
acres = _.acres.sum(),
|
41 |
-
mean_richness = (_.richness * _.acres).sum() / _.acres.sum(),
|
42 |
-
mean_rsr = (_.rsr * _.acres).sum() / _.acres.sum(),
|
43 |
-
mean_irrecoverable_carbon = (_.irrecoverable_carbon * _.acres).sum() / _.acres.sum(),
|
44 |
-
mean_manageable_carbon = (_.manageable_carbon * _.acres).sum() / _.acres.sum(),
|
45 |
-
mean_fire = (_.fire *_.acres).sum()/_.acres.sum(),
|
46 |
-
mean_rxburn = (_.rxburn *_.acres).sum()/_.acres.sum(),
|
47 |
-
mean_disadvantaged = (_.disadvantaged_communities * _.acres).sum() / _.acres.sum(),
|
48 |
-
mean_svi = (_.svi * _.acres).sum() / _.acres.sum(),
|
49 |
-
)
|
50 |
-
.mutate(percent_CA=_.percent_CA.round(3),
|
51 |
-
acres=_.acres.round(0))
|
52 |
-
)
|
53 |
-
# if colors is not None and not colors.empty:
|
54 |
-
df = df.inner_join(group_totals, main_group)
|
55 |
-
df = df.mutate(percent_group=( _.acres / _.total_acres).round(3))
|
56 |
-
if colors is not None and not colors.empty: #only the df will have colors, df_tab doesn't since we are printing it.
|
57 |
-
df = df.inner_join(colors, column[-1])
|
58 |
-
df = df.cast({col: "string" for col in column})
|
59 |
-
df = df.to_pandas()
|
60 |
-
return df
|
61 |
-
|
62 |
-
|
63 |
-
def summary_table(ca, column, select_colors, color_choice, filter_cols, filter_vals,colorby_vals): # get df for charts + df_tab for printed table
|
64 |
-
colors = colorTable(select_colors,color_choice,column)
|
65 |
-
filters = []
|
66 |
-
if filter_cols and filter_vals: #if a filter is selected, add to list of filters
|
67 |
-
for filter_col, filter_val in zip(filter_cols, filter_vals):
|
68 |
-
if len(filter_val) > 1:
|
69 |
-
filters.append(getattr(_, filter_col).isin(filter_val))
|
70 |
-
else:
|
71 |
-
filters.append(getattr(_, filter_col) == filter_val[0])
|
72 |
-
if column not in filter_cols: #show color_by column in table by adding it as a filter (if it's not already a filter)
|
73 |
-
filter_cols.append(column)
|
74 |
-
filters.append(getattr(_, column).isin(colorby_vals[column]))
|
75 |
-
combined_filter = reduce(lambda x, y: x & y, filters) #combining all the filters into ibis filter expression
|
76 |
|
77 |
-
|
78 |
-
df_percent = get_summary(ca, only_conserved, [column],column, colors) # df used for percentage, excludes non-conserved.
|
79 |
-
|
80 |
-
df_tab = get_summary(ca, combined_filter, filter_cols, column, colors = None) #df used for printed table
|
81 |
-
|
82 |
-
if "non-conserved" in list(chain.from_iterable(filter_vals)):
|
83 |
-
combined_filter = (combined_filter) | (_.status.isin(['non-conserved']))
|
84 |
-
|
85 |
-
df = get_summary(ca, combined_filter, [column], column, colors) # df used for charts
|
86 |
-
|
87 |
-
df_bar_30x30 = None # no stacked charts if we have status/gap_code
|
88 |
-
if column not in ["status","gap_code"]: # df for stacked 30x30 status bar chart
|
89 |
-
colors = colorTable(select_colors,"30x30 Status",'status')
|
90 |
-
df_bar_30x30 = get_summary(ca, combined_filter, [column, 'status'], column, colors) # df used for charts
|
91 |
|
92 |
-
|
93 |
-
return df, df_tab, df_percent, df_bar_30x30
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
def summary_table_sql(ca, column, colors, ids): # get df for charts + df_tab for printed table
|
99 |
-
filters = [_.id.isin(ids)]
|
100 |
-
combined_filter = reduce(lambda x, y: x & y, filters) #combining all the filters into ibis filter expression
|
101 |
-
df = get_summary(ca, combined_filter, [column], column, colors) # df used for charts
|
102 |
-
return df
|
103 |
-
|
104 |
-
|
105 |
-
def get_hex(df, color,sort_order):
|
106 |
-
return list(df.drop_duplicates(subset=color, keep="first")
|
107 |
-
.set_index(color)
|
108 |
-
.reindex(sort_order)
|
109 |
-
.dropna()["color"])
|
110 |
-
|
111 |
-
def transform_label(label, x_field):
|
112 |
-
# converting labels for that gnarly stacked bar chart
|
113 |
-
if x_field == "access_type":
|
114 |
-
return label.replace(" Access", "")
|
115 |
-
elif x_field == "ecoregion":
|
116 |
-
label = label.replace("Northern California", "NorCal")
|
117 |
-
label = label.replace("Southern California", "SoCal")
|
118 |
-
label = label.replace("Southeastern", "SE.")
|
119 |
-
label = label.replace("Northwestern", "NW.")
|
120 |
-
label = label.replace("and", "&")
|
121 |
-
label = label.replace("California", "CA")
|
122 |
-
return label
|
123 |
-
else:
|
124 |
-
return label
|
125 |
-
|
126 |
-
|
127 |
-
def stacked_bar(df, x, y, color, title, colors):
|
128 |
-
label_colors = colors['color'].to_list()
|
129 |
-
# bar order
|
130 |
-
if x == "established": # order labels in chronological order, not alphabetic.
|
131 |
-
sort = '-x'
|
132 |
-
elif x == "access_type": # order based on levels of openness
|
133 |
-
sort = ['Open', 'Restricted', 'No Public', "Unknown"]
|
134 |
-
elif x == "easement":
|
135 |
-
sort = ['True', 'False']
|
136 |
-
elif x == "manager_type":
|
137 |
-
sort = ["Federal", "Tribal", "State", "Special District", "County", "City", "HOA",
|
138 |
-
"Joint", "Non Profit", "Private", "Unknown"]
|
139 |
-
elif x == "status":
|
140 |
-
sort = ["30x30-conserved", "other-conserved", "unknown", "non-conserved"]
|
141 |
-
elif x == "ecoregion":
|
142 |
-
sort = ['SE. Great Basin', 'Mojave Desert', 'Sonoran Desert', 'Sierra Nevada',
|
143 |
-
'SoCal Mountains & Valleys', 'Mono', 'Central CA Coast', 'Klamath Mountains',
|
144 |
-
'NorCal Coast', 'NorCal Coast Ranges', 'NW. Basin & Range', 'Colorado Desert',
|
145 |
-
'Central Valley Coast Ranges', 'SoCal Coast', 'Sierra Nevada Foothills',
|
146 |
-
'Southern Cascades', 'Modoc Plateau', 'Great Valley (North)',
|
147 |
-
'NorCal Interior Coast Ranges', 'Great Valley (South)']
|
148 |
-
else:
|
149 |
-
sort = 'x'
|
150 |
-
|
151 |
-
if x == "manager_type":
|
152 |
-
angle = 270
|
153 |
-
height = 350
|
154 |
-
|
155 |
-
elif x == 'ecoregion':
|
156 |
-
angle = 270
|
157 |
-
height = 430
|
158 |
-
else:
|
159 |
-
angle = 0
|
160 |
-
height = 310
|
161 |
-
|
162 |
-
# stacked bar order
|
163 |
-
sort_order = ['30x30-conserved', 'other-conserved', 'unknown', 'non-conserved']
|
164 |
-
y_titles = {
|
165 |
-
'ecoregion': 'Ecoregion (%)',
|
166 |
-
'established': 'Year (%)',
|
167 |
-
'manager_type': 'Manager Type (%)',
|
168 |
-
'easement': 'Easement (%)',
|
169 |
-
'access_type': 'Access (%)'
|
170 |
-
}
|
171 |
-
ytitle = y_titles.get(x, y)
|
172 |
-
color_hex = get_hex(df[[color, 'color']], color, sort_order)
|
173 |
-
sort_order = sort_order[0:len(color_hex)]
|
174 |
-
df["stack_order"] = df[color].apply(lambda val: sort_order.index(val) if val in sort_order else len(sort_order))
|
175 |
-
|
176 |
-
# shorten labels to fit on chart
|
177 |
-
label_transform = f"datum.{x}"
|
178 |
-
if x == "access_type":
|
179 |
-
label_transform = f"replace(datum.{x}, ' Access', '')"
|
180 |
-
elif x == "ecoregion":
|
181 |
-
label_transform = (
|
182 |
-
"replace("
|
183 |
-
"replace("
|
184 |
-
"replace("
|
185 |
-
"replace("
|
186 |
-
"replace("
|
187 |
-
"replace(datum.ecoregion, 'Northern California', 'NorCal'),"
|
188 |
-
"'Southern California', 'SoCal'),"
|
189 |
-
"'Southeastern', 'SE.'),"
|
190 |
-
"'Northwestern', 'NW.'),"
|
191 |
-
"'and', '&'),"
|
192 |
-
"'California', 'CA')"
|
193 |
-
)
|
194 |
-
|
195 |
-
# to match the colors in the map to each label, need to write some ugly code..
|
196 |
-
# bar chart w/ xlabels hidden
|
197 |
-
chart = alt.Chart(df).mark_bar(height = 500).transform_calculate(
|
198 |
-
xlabel=label_transform
|
199 |
-
).encode(
|
200 |
-
x=alt.X("xlabel:N", sort=sort, title=None,
|
201 |
-
axis=alt.Axis(labelLimit=150, labelAngle=angle, labelColor="transparent")),
|
202 |
-
y=alt.Y(y, title=ytitle, axis=alt.Axis(labelPadding=5)).scale(domain=(0, 1)),
|
203 |
-
color=alt.Color(
|
204 |
-
color,
|
205 |
-
sort=sort_order,
|
206 |
-
scale=alt.Scale(domain=sort_order, range=color_hex)
|
207 |
-
),
|
208 |
-
order=alt.Order("stack_order:Q", sort="ascending"),
|
209 |
-
tooltip=[
|
210 |
-
alt.Tooltip(x, type="nominal"),
|
211 |
-
alt.Tooltip(color, type="nominal"),
|
212 |
-
alt.Tooltip("percent_group", type="quantitative", format=",.1%"),
|
213 |
-
alt.Tooltip("acres", type="quantitative", format=",.0f"),
|
214 |
-
]
|
215 |
-
)
|
216 |
-
transformed_labels = [transform_label(str(lab), x) for lab in colors[x]]
|
217 |
-
labels_df = colors
|
218 |
-
labels_df['xlabel'] = transformed_labels
|
219 |
-
# 2 layers, 1 for the symbol and 1 for the text
|
220 |
-
if angle == 0:
|
221 |
-
symbol_layer = alt.Chart(labels_df).mark_point(
|
222 |
-
filled=True,
|
223 |
-
shape="circle",
|
224 |
-
size=100,
|
225 |
-
xOffset = 0,
|
226 |
-
yOffset=130,
|
227 |
-
align = 'left',
|
228 |
-
tooltip = False
|
229 |
-
).encode(
|
230 |
-
x=alt.X("xlabel:N", sort=sort),
|
231 |
-
color=alt.Color("color:N", scale=None)
|
232 |
-
)
|
233 |
-
text_layer = alt.Chart(labels_df).mark_text(
|
234 |
-
dy=115, # shifts the text to the right of the symbol
|
235 |
-
dx = 0,
|
236 |
-
yOffset=0,
|
237 |
-
xOffset = 0,
|
238 |
-
align='center',
|
239 |
-
color="black",
|
240 |
-
tooltip = False
|
241 |
-
).encode(
|
242 |
-
x=alt.X("xlabel:N", sort=sort),
|
243 |
-
text=alt.Text("xlabel:N")
|
244 |
-
)
|
245 |
-
# vertical labels
|
246 |
-
elif angle == 270:
|
247 |
-
symbol_layer = alt.Chart(labels_df).mark_point(
|
248 |
-
xOffset = 0,
|
249 |
-
yOffset= 100,
|
250 |
-
filled=True,
|
251 |
-
shape="circle",
|
252 |
-
size=100,
|
253 |
-
tooltip = False
|
254 |
-
).encode(
|
255 |
-
x=alt.X("xlabel:N", sort=sort),
|
256 |
-
color=alt.Color("color:N", scale=None)
|
257 |
-
)
|
258 |
-
text_layer = alt.Chart(labels_df).mark_text(
|
259 |
-
dy=0,
|
260 |
-
dx = -110,
|
261 |
-
angle=270,
|
262 |
-
align='right',
|
263 |
-
color="black",
|
264 |
-
tooltip = False
|
265 |
-
).encode(
|
266 |
-
x=alt.X("xlabel:N", sort=sort),
|
267 |
-
text=alt.Text("xlabel:N")
|
268 |
-
)
|
269 |
-
|
270 |
-
custom_labels = alt.layer(symbol_layer, text_layer)
|
271 |
-
final_chart = alt.layer(chart, custom_labels)
|
272 |
-
|
273 |
-
# put it all together
|
274 |
-
final_chart = final_chart.properties(
|
275 |
-
width="container",
|
276 |
-
height=height,
|
277 |
-
title=title
|
278 |
-
).configure_legend(
|
279 |
-
direction='horizontal',
|
280 |
-
orient='top',
|
281 |
-
columns=2,
|
282 |
-
title=None,
|
283 |
-
labelOffset=2,
|
284 |
-
offset=10,
|
285 |
-
symbolType = 'square'
|
286 |
-
).configure_title(
|
287 |
-
fontSize=18, align="center", anchor='middle', offset=10
|
288 |
-
)
|
289 |
-
return final_chart
|
290 |
-
|
291 |
-
def area_plot(df, column): # Percent protected pie chart
|
292 |
-
base = alt.Chart(df).encode(
|
293 |
-
alt.Theta("percent_CA:Q").stack(True),
|
294 |
-
)
|
295 |
-
pie = (
|
296 |
-
base
|
297 |
-
.mark_arc(innerRadius=40, outerRadius=100, stroke="black", strokeWidth=0.5)
|
298 |
-
.encode(
|
299 |
-
alt.Color("color:N").scale(None).legend(None),
|
300 |
-
tooltip=[
|
301 |
-
alt.Tooltip(column, type="nominal"),
|
302 |
-
alt.Tooltip("percent_CA", type="quantitative", format=",.1%"),
|
303 |
-
alt.Tooltip("acres", type="quantitative", format=",.0f"),
|
304 |
-
]
|
305 |
-
)
|
306 |
-
)
|
307 |
-
text = (
|
308 |
-
base
|
309 |
-
.mark_text(radius=80, size=14, color="white")
|
310 |
-
.encode(text=column + ":N")
|
311 |
-
)
|
312 |
-
plot = pie # pie + text
|
313 |
-
return plot.properties(width="container", height=290)
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
def bar_chart(df, x, y, title): #display summary stats for color_by column
|
318 |
-
#axis label angles / chart size
|
319 |
-
if x == "manager_type": #labels are too long, making vertical
|
320 |
-
angle = 270
|
321 |
-
height = 373
|
322 |
-
elif x == 'ecoregion': # make labels vertical and figure taller
|
323 |
-
angle = 270
|
324 |
-
height = 430
|
325 |
-
else: #other labels are horizontal
|
326 |
-
angle = 0
|
327 |
-
height = 310
|
328 |
-
|
329 |
-
# order of bars
|
330 |
-
sort = 'x'
|
331 |
-
lineBreak = ''
|
332 |
-
if x == "established": # order labels in chronological order, not alphabetic.
|
333 |
-
sort = '-x'
|
334 |
-
elif x == "access_type": #order based on levels of openness
|
335 |
-
sort=['Open', 'Restricted', 'No Public', "Unknown"]
|
336 |
-
elif x == "easement":
|
337 |
-
sort=['True','False']
|
338 |
-
elif x == "manager_type":
|
339 |
-
sort = ["Federal","Tribal","State","Special District", "County", "City", "HOA","Joint","Non Profit","Private","Unknown"]
|
340 |
-
elif x == "ecoregion":
|
341 |
-
sort = ['SE. Great Basin','Mojave Desert','Sonoran Desert','Sierra Nevada','SoCal Mountains & Valleys','Mono',
|
342 |
-
'Central CA Coast','Klamath Mountains','NorCal Coast','NorCal Coast Ranges',
|
343 |
-
'NW. Basin & Range','Colorado Desert','Central Valley Coast Ranges','SoCal Coast',
|
344 |
-
'Sierra Nevada Foothills','Southern Cascades','Modoc Plateau','Great Valley (North)','NorCal Interior Coast Ranges',
|
345 |
-
'Great Valley (South)']
|
346 |
-
elif x == "status":
|
347 |
-
sort = ["30x30-conserved","other-conserved","unknown","non-conserved"]
|
348 |
-
lineBreak = '-'
|
349 |
-
|
350 |
-
# modify label names in bar chart to fit in frame
|
351 |
-
label_transform = f"datum.{x}" # default; no change
|
352 |
-
if x == "access_type":
|
353 |
-
label_transform = f"replace(datum.{x}, ' Access', '')" #omit 'access' from access_type
|
354 |
-
elif x == "ecoregion":
|
355 |
-
label_transform = (
|
356 |
-
"replace("
|
357 |
-
"replace("
|
358 |
-
"replace("
|
359 |
-
"replace("
|
360 |
-
"replace("
|
361 |
-
"replace(datum.ecoregion, 'Northern California', 'NorCal'),"
|
362 |
-
"'Southern California', 'SoCal'),"
|
363 |
-
"'Southeastern', 'SE.'),"
|
364 |
-
"'Northwestern', 'NW.'),"
|
365 |
-
"'and', '&'),"
|
366 |
-
"'California', 'CA')"
|
367 |
-
)
|
368 |
-
y_titles = {
|
369 |
-
'mean_richness': 'Richness (Mean)',
|
370 |
-
'mean_rsr': 'Range-Size Rarity (Mean)',
|
371 |
-
'mean_irrecoverable_carbon': 'Irrecoverable Carbon (Mean)',
|
372 |
-
'mean_manageable_carbon': 'Manageable Carbon (Mean)',
|
373 |
-
'mean_disadvantaged': 'Disadvantaged (Mean)',
|
374 |
-
'mean_svi': 'SVI (Mean)',
|
375 |
-
'mean_fire': 'Fire (Mean)',
|
376 |
-
'mean_rxburn': 'Rx Fire (Mean)'
|
377 |
-
}
|
378 |
-
ytitle = y_titles.get(y, y) # Default to `y` if not in the dictionary
|
379 |
-
|
380 |
-
x_title = next(key for key, value in select_column.items() if value == x)
|
381 |
-
chart = alt.Chart(df).mark_bar(stroke = 'black', strokeWidth = .5).transform_calculate(
|
382 |
-
label=label_transform
|
383 |
-
).encode(
|
384 |
-
x=alt.X("label:N",
|
385 |
-
axis=alt.Axis(labelAngle=angle, title=x_title, labelLimit = 200),
|
386 |
-
sort=sort),
|
387 |
-
y=alt.Y(y, axis=alt.Axis(title = ytitle)),
|
388 |
-
color=alt.Color('color').scale(None),
|
389 |
-
).configure(lineBreak = lineBreak)
|
390 |
-
|
391 |
-
chart = chart.properties(width="container", height=height, title = title
|
392 |
-
).configure_title(fontSize=18, align = "center",anchor='middle')
|
393 |
-
return chart
|
394 |
-
|
395 |
|
396 |
|
397 |
def sync_checkboxes(source):
|
|
|
|
|
|
|
398 |
# gap 1 and gap 2 on -> 30x30-conserved on
|
399 |
if source in ["gap_code1", "gap_code2"]:
|
400 |
st.session_state['status30x30-conserved'] = st.session_state.gap_code1 and st.session_state.gap_code2
|
@@ -428,62 +73,103 @@ def sync_checkboxes(source):
|
|
428 |
st.session_state.gap_code0 = st.session_state['statusnon-conserved']
|
429 |
|
430 |
|
431 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
432 |
column = style_options[style_choice]['property']
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
441 |
|
|
|
|
|
|
|
|
|
442 |
|
|
|
|
|
|
|
443 |
|
444 |
-
|
445 |
-
|
446 |
-
column = style_options[style_choice]['property']
|
447 |
-
opts = [style[0] for style in style_options[style_choice]['stops']]
|
448 |
-
d = {}
|
449 |
-
d[column] = opts
|
450 |
-
return d
|
451 |
|
|
|
|
|
|
|
|
|
452 |
|
453 |
-
|
454 |
-
|
455 |
-
position = 'bottom-left'
|
456 |
-
fontsize = 15
|
457 |
-
bg_color = 'white'
|
458 |
-
# shorten legend for ecoregions
|
459 |
-
if color_choice == "Ecoregion":
|
460 |
-
legend = {key.replace("Northern California", "NorCal"): value for key, value in legend.items()}
|
461 |
-
legend = {key.replace("Southern California", "SoCal"): value for key, value in legend.items()}
|
462 |
-
legend = {key.replace("Southeastern", "SE."): value for key, value in legend.items()}
|
463 |
-
legend = {key.replace("and", "&"): value for key, value in legend.items()}
|
464 |
-
legend = {key.replace("California", "CA"): value for key, value in legend.items()}
|
465 |
-
legend = {key.replace("Northwestern", "NW."): value for key, value in legend.items()}
|
466 |
-
bg_color = 'rgba(255, 255, 255, 0.6)'
|
467 |
-
fontsize = 12
|
468 |
-
return legend, position, bg_color, fontsize
|
469 |
|
|
|
|
|
|
|
470 |
|
471 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
472 |
def get_pmtiles_style(paint, alpha, filter_cols, filter_vals):
|
473 |
-
|
474 |
-
for
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
|
|
|
|
|
|
|
480 |
"version": 8,
|
481 |
-
"sources": {
|
482 |
-
"ca": {
|
483 |
-
"type": "vector",
|
484 |
-
"url": "pmtiles://" + ca_pmtiles,
|
485 |
-
}
|
486 |
-
},
|
487 |
"layers": [
|
488 |
{
|
489 |
"id": "ca30x30",
|
@@ -491,39 +177,232 @@ def get_pmtiles_style(paint, alpha, filter_cols, filter_vals):
|
|
491 |
"source-layer": "ca30x30",
|
492 |
"type": "fill",
|
493 |
"filter": combined_filters,
|
494 |
-
"paint": {
|
495 |
-
"fill-color": paint,
|
496 |
-
"fill-opacity": alpha
|
497 |
-
}
|
498 |
}
|
499 |
-
]
|
500 |
}
|
501 |
-
return style
|
502 |
-
|
503 |
|
504 |
def get_pmtiles_style_llm(paint, ids):
|
505 |
-
|
506 |
-
style
|
|
|
|
|
507 |
"version": 8,
|
508 |
-
"sources": {
|
509 |
-
"ca": {
|
510 |
-
"type": "vector",
|
511 |
-
"url": "pmtiles://" + ca_pmtiles,
|
512 |
-
}
|
513 |
-
},
|
514 |
"layers": [
|
515 |
{
|
516 |
"id": "ca30x30",
|
517 |
"source": "ca",
|
518 |
"source-layer": "ca30x30",
|
519 |
"type": "fill",
|
520 |
-
"filter":
|
521 |
-
"
|
522 |
-
|
523 |
-
"fill-opacity": 1,
|
524 |
-
}
|
525 |
}
|
526 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
527 |
}
|
528 |
-
return style
|
529 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import streamlit as st
|
2 |
import streamlit.components.v1 as components
|
3 |
+
import pandas as pd
|
4 |
import leafmap.maplibregl as leafmap
|
5 |
import altair as alt
|
6 |
import ibis
|
7 |
from ibis import _
|
8 |
import ibis.selectors as s
|
9 |
import os
|
|
|
|
|
10 |
from shapely import wkb
|
|
|
|
|
11 |
from typing import Optional
|
12 |
from functools import reduce
|
13 |
from itertools import chain
|
14 |
|
15 |
from variables import *
|
16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
+
######################## UI FUNCTIONS
|
19 |
+
def get_buttons(style_options, style_choice, default_boxes=None):
|
20 |
+
"""
|
21 |
+
Creates Streamlit checkboxes based on style options and returns the selected filters.
|
22 |
+
"""
|
23 |
+
column = style_options[style_choice]['property']
|
24 |
+
opts = [style[0] for style in style_options[style_choice]['stops']]
|
25 |
+
default_boxes = default_boxes or {}
|
26 |
|
27 |
+
buttons = {}
|
28 |
+
for name in opts:
|
29 |
+
key = column + str(name)
|
30 |
+
buttons[name] = st.checkbox(f"{name}", value=st.session_state[key], key=key, on_change = sync_checkboxes, args = (key,))
|
31 |
+
filter_choice = [key for key, value in buttons.items() if value]
|
32 |
+
return {column: filter_choice}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
|
34 |
+
# buttons = {name: st.checkbox(name, value=st.session_state.get(column + str(name), False), key=column + str(name), on_change=sync_checkboxes, args=(column + str(name),)) for name in opts}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
|
36 |
+
# return {column: [key for key, value in buttons.items() if value]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
|
38 |
|
39 |
def sync_checkboxes(source):
|
40 |
+
"""
|
41 |
+
Synchronizes checkbox selections in Streamlit based on 30x30 status and GAP codes.
|
42 |
+
"""
|
43 |
# gap 1 and gap 2 on -> 30x30-conserved on
|
44 |
if source in ["gap_code1", "gap_code2"]:
|
45 |
st.session_state['status30x30-conserved'] = st.session_state.gap_code1 and st.session_state.gap_code2
|
|
|
73 |
st.session_state.gap_code0 = st.session_state['statusnon-conserved']
|
74 |
|
75 |
|
76 |
+
def color_table(select_colors, color_choice, column):
|
77 |
+
"""
|
78 |
+
Converts selected color mapping into a DataFrame.
|
79 |
+
"""
|
80 |
+
return ibis.memtable(select_colors[color_choice], columns=[column, "color"]).to_pandas()
|
81 |
+
|
82 |
+
def get_color_vals(style_options, style_choice):
|
83 |
+
"""
|
84 |
+
Extracts available color values for a selected style option.
|
85 |
+
"""
|
86 |
column = style_options[style_choice]['property']
|
87 |
+
return {column: [style[0] for style in style_options[style_choice]['stops']]}
|
88 |
+
|
89 |
+
|
90 |
+
|
91 |
+
######################## SUMMARY & DATA FUNCTIONS
|
92 |
+
def get_summary(ca, combined_filter, column, main_group, colors = None):
|
93 |
+
"""
|
94 |
+
Computes summary statistics for the filtered dataset.
|
95 |
+
"""
|
96 |
+
df = ca.filter(combined_filter)
|
97 |
+
|
98 |
+
#total acres for each group
|
99 |
+
group_totals = df.group_by(main_group).aggregate(total_acres=_.acres.sum())
|
100 |
+
df = (df.group_by(*column)
|
101 |
+
.aggregate(percent_CA=(_.acres.sum() / ca_area_acres),
|
102 |
+
acres=_.acres.sum(),
|
103 |
+
mean_richness=(_.richness * _.acres).sum() / _.acres.sum(),
|
104 |
+
mean_rsr=(_.rsr * _.acres).sum() / _.acres.sum(),
|
105 |
+
mean_irrecoverable_carbon=(_.irrecoverable_carbon * _.acres).sum() / _.acres.sum(),
|
106 |
+
mean_manageable_carbon=(_.manageable_carbon * _.acres).sum() / _.acres.sum(),
|
107 |
+
mean_fire=(_.fire * _.acres).sum()/_.acres.sum(),
|
108 |
+
mean_rxburn=(_.rxburn * _.acres).sum()/_.acres.sum(),
|
109 |
+
mean_disadvantaged=(_.disadvantaged_communities * _.acres).sum() / _.acres.sum(),
|
110 |
+
mean_svi=(_.svi * _.acres).sum() / _.acres.sum())
|
111 |
+
.mutate(percent_CA=_.percent_CA.round(3), acres=_.acres.round(0)))
|
112 |
+
df = df.inner_join(group_totals, main_group).mutate(percent_group=( _.acres / _.total_acres).round(3))
|
113 |
+
if colors is not None and not colors.empty:
|
114 |
+
df = df.inner_join(colors, column[-1])
|
115 |
+
return df.cast({col: "string" for col in column}).execute()
|
116 |
+
|
117 |
+
def get_summary_table(ca, column, select_colors, color_choice, filter_cols, filter_vals, colorby_vals):
|
118 |
+
"""
|
119 |
+
Generates summary tables for visualization and reporting.
|
120 |
+
"""
|
121 |
+
colors = color_table(select_colors, color_choice, column)
|
122 |
+
|
123 |
+
#if a filter is selected, add to list of filters
|
124 |
+
filters = [getattr(_, col).isin(vals) for col, vals in zip(filter_cols, filter_vals) if vals]
|
125 |
|
126 |
+
#show color_by column in table by adding it as a filter (if it's not already a filter)
|
127 |
+
if column not in filter_cols:
|
128 |
+
filter_cols.append(column)
|
129 |
+
filters.append(getattr(_, column).isin(colorby_vals[column]))
|
130 |
|
131 |
+
#combining all the filters into ibis filter expression
|
132 |
+
combined_filter = reduce(lambda x, y: x & y, filters)
|
133 |
+
only_conserved = combined_filter & (_.status.isin(['30x30-conserved']))
|
134 |
|
135 |
+
# df used for percentage, excludes non-conserved.
|
136 |
+
df_percent = get_summary(ca, only_conserved, [column], column, colors)
|
|
|
|
|
|
|
|
|
|
|
137 |
|
138 |
+
#df used for printed table
|
139 |
+
df_tab = get_summary(ca, combined_filter, filter_cols, column, colors=None)
|
140 |
+
if "non-conserved" in chain.from_iterable(filter_vals):
|
141 |
+
combined_filter = combined_filter | (_.status.isin(['non-conserved']))
|
142 |
|
143 |
+
# df used for charts
|
144 |
+
df = get_summary(ca, combined_filter, [column], column, colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
145 |
|
146 |
+
# df for stacked 30x30 status bar chart
|
147 |
+
df_bar_30x30 = None if column in ["status", "gap_code"] else get_summary(ca, combined_filter, [column, 'status'], column, color_table(select_colors, "30x30 Status", 'status'))
|
148 |
+
return df, df_tab, df_percent, df_bar_30x30
|
149 |
|
150 |
|
151 |
+
def get_summary_table_sql(ca, column, colors, ids):
|
152 |
+
"""
|
153 |
+
Generates a summary table using specific IDs as filters.
|
154 |
+
"""
|
155 |
+
combined_filter = _.id.isin(ids)
|
156 |
+
return get_summary(ca, combined_filter, [column], column, colors)
|
157 |
+
|
158 |
+
|
159 |
+
######################## MAP STYLING FUNCTIONS
|
160 |
def get_pmtiles_style(paint, alpha, filter_cols, filter_vals):
|
161 |
+
"""
|
162 |
+
Generates a MapLibre GL style for PMTiles with specified filters.
|
163 |
+
"""
|
164 |
+
filters = [["match", ["get", col], val, True, False] for col, val in zip(filter_cols, filter_vals)]
|
165 |
+
combined_filters = ["all", *filters]
|
166 |
+
|
167 |
+
if "non-conserved" in chain.from_iterable(filter_vals):
|
168 |
+
combined_filters = ["any", combined_filters, ["match", ["get", "status"], ["non-conserved"], True, False]]
|
169 |
+
|
170 |
+
return {
|
171 |
"version": 8,
|
172 |
+
"sources": {"ca": {"type": "vector", "url": f"pmtiles://{ca_pmtiles}"}},
|
|
|
|
|
|
|
|
|
|
|
173 |
"layers": [
|
174 |
{
|
175 |
"id": "ca30x30",
|
|
|
177 |
"source-layer": "ca30x30",
|
178 |
"type": "fill",
|
179 |
"filter": combined_filters,
|
180 |
+
"paint": {"fill-color": paint, "fill-opacity": alpha},
|
|
|
|
|
|
|
181 |
}
|
182 |
+
],
|
183 |
}
|
|
|
|
|
184 |
|
185 |
def get_pmtiles_style_llm(paint, ids):
|
186 |
+
"""
|
187 |
+
Generates a MapLibre GL style for PMTiles using specific IDs as filters.
|
188 |
+
"""
|
189 |
+
return {
|
190 |
"version": 8,
|
191 |
+
"sources": {"ca": {"type": "vector", "url": f"pmtiles://{ca_pmtiles}"}},
|
|
|
|
|
|
|
|
|
|
|
192 |
"layers": [
|
193 |
{
|
194 |
"id": "ca30x30",
|
195 |
"source": "ca",
|
196 |
"source-layer": "ca30x30",
|
197 |
"type": "fill",
|
198 |
+
"filter": ["in", ["get", "id"], ["literal", ids]],
|
199 |
+
# "filter": ["all", ["match", ["get", "id"], ids, True, False]],
|
200 |
+
"paint": {"fill-color": paint, "fill-opacity": 1},
|
|
|
|
|
201 |
}
|
202 |
+
],
|
203 |
+
}
|
204 |
+
|
205 |
+
def get_legend(style_options, color_choice):
|
206 |
+
"""
|
207 |
+
Generates a legend dictionary with color mapping and formatting adjustments.
|
208 |
+
"""
|
209 |
+
legend = {cat: color for cat, color in style_options[color_choice]['stops']}
|
210 |
+
position, fontsize, bg_color = 'bottom-left', 15, 'white'
|
211 |
+
|
212 |
+
# shorten legend for ecoregions
|
213 |
+
if color_choice == "Ecoregion":
|
214 |
+
legend = {key.replace("Northern California", "NorCal"): value for key, value in legend.items()}
|
215 |
+
legend = {key.replace("Southern California", "SoCal"): value for key, value in legend.items()}
|
216 |
+
legend = {key.replace("Southeastern", "SE."): value for key, value in legend.items()}
|
217 |
+
legend = {key.replace("and", "&"): value for key, value in legend.items()}
|
218 |
+
legend = {key.replace("California", "CA"): value for key, value in legend.items()}
|
219 |
+
legend = {key.replace("Northwestern", "NW."): value for key, value in legend.items()}
|
220 |
+
bg_color = 'rgba(255, 255, 255, 0.6)'
|
221 |
+
fontsize = 12
|
222 |
+
return legend, position, bg_color, fontsize
|
223 |
+
|
224 |
+
|
225 |
+
|
226 |
+
|
227 |
+
######################## CHART FUNCTIONS
|
228 |
+
def area_chart(df, column):
|
229 |
+
"""
|
230 |
+
Generates an Altair pie chart representing the percentage of protected areas.
|
231 |
+
"""
|
232 |
+
base = alt.Chart(df).encode(alt.Theta("percent_CA:Q").stack(True))
|
233 |
+
pie = (
|
234 |
+
base.mark_arc(innerRadius=40, outerRadius=100, stroke="black", strokeWidth=0.1)
|
235 |
+
.encode(
|
236 |
+
alt.Color("color:N").scale(None).legend(None),
|
237 |
+
tooltip=[
|
238 |
+
alt.Tooltip(column, type="nominal"),
|
239 |
+
alt.Tooltip("percent_CA", type="quantitative", format=",.1%"),
|
240 |
+
alt.Tooltip("acres", type="quantitative", format=",.0f"),
|
241 |
+
]
|
242 |
+
)
|
243 |
+
)
|
244 |
+
return pie.properties(width="container", height=290)
|
245 |
+
|
246 |
+
|
247 |
+
def bar_chart(df, x, y, title):
|
248 |
+
"""Creates a simple bar chart."""
|
249 |
+
return create_bar_chart(df, x, y, title)
|
250 |
+
|
251 |
+
def stacked_bar(df, x, y, color, title, colors):
|
252 |
+
"""Creates a stacked bar chart."""
|
253 |
+
return create_bar_chart(df, x, y, title, color=color, stacked=True, colors=colors)
|
254 |
+
|
255 |
+
|
256 |
+
def get_chart_settings(x, stacked):
|
257 |
+
"""
|
258 |
+
Returns sorting, axis settings, and y-axis title mappings.
|
259 |
+
"""
|
260 |
+
sort_options = {
|
261 |
+
"established": "-x",
|
262 |
+
"access_type": ["Open", "Restricted", "No Public", "Unknown"],
|
263 |
+
"easement": ["True", "False"],
|
264 |
+
"manager_type": ["Federal", "Tribal", "State", "Special District", "County", "City",
|
265 |
+
"HOA", "Joint", "Non Profit", "Private", "Unknown"],
|
266 |
+
"status": ["30x30-conserved", "other-conserved", "unknown", "non-conserved"],
|
267 |
+
"ecoregion": ['SE. Great Basin', 'Mojave Desert', 'Sonoran Desert', 'Sierra Nevada',
|
268 |
+
'SoCal Mountains & Valleys', 'Mono', 'Central CA Coast', 'Klamath Mountains',
|
269 |
+
'NorCal Coast', 'NorCal Coast Ranges', 'NW. Basin & Range', 'Colorado Desert',
|
270 |
+
'Central Valley Coast Ranges', 'SoCal Coast', 'Sierra Nevada Foothills',
|
271 |
+
'Southern Cascades', 'Modoc Plateau', 'Great Valley (North)',
|
272 |
+
'NorCal Interior Coast Ranges', 'Great Valley (South)']
|
273 |
}
|
|
|
274 |
|
275 |
+
y_titles = {
|
276 |
+
"ecoregion": "Ecoregion (%)", "established": "Year (%)",
|
277 |
+
"manager_type": "Manager Type (%)", "easement": "Easement (%)",
|
278 |
+
"access_type": "Access (%)", "mean_richness": "Richness (Mean)",
|
279 |
+
"mean_rsr": "Range-Size Rarity (Mean)", "mean_irrecoverable_carbon": "Irrecoverable Carbon (Mean)",
|
280 |
+
"mean_manageable_carbon": "Manageable Carbon (Mean)", "mean_disadvantaged": "Disadvantaged (Mean)",
|
281 |
+
"mean_svi": "SVI (Mean)", "mean_fire": "Fire (Mean)", "mean_rxburn": "Rx Fire (Mean)"
|
282 |
+
}
|
283 |
+
|
284 |
+
angle = 270 if x in ["manager_type", "ecoregion"] else 0
|
285 |
+
height = 250 if stacked else 400 if x == "ecoregion" else 350 if x == "manager_type" else 300
|
286 |
+
|
287 |
+
return sort_options.get(x, "x"), angle, height, y_titles.get(x, x)
|
288 |
+
|
289 |
+
|
290 |
+
def get_label_transform(x, label=None):
|
291 |
+
"""
|
292 |
+
Returns label transformation logic for Altair expressions and manual label conversion.
|
293 |
+
"""
|
294 |
+
transformations = {
|
295 |
+
"access_type": ("replace(datum.access_type, ' Access', '')", lambda lbl: lbl.replace(" Access", "")),
|
296 |
+
"ecoregion": (
|
297 |
+
"replace(replace(replace(replace(replace("
|
298 |
+
"replace(datum.ecoregion, 'Northern California', 'NorCal'),"
|
299 |
+
"'Southern California', 'SoCal'),"
|
300 |
+
"'Southeastern', 'SE.'),"
|
301 |
+
"'Northwestern', 'NW.'),"
|
302 |
+
"'and', '&'),"
|
303 |
+
"'California', 'CA')",
|
304 |
+
lambda lbl: (lbl.replace("Northern California", "NorCal")
|
305 |
+
.replace("Southern California", "SoCal")
|
306 |
+
.replace("Southeastern", "SE.")
|
307 |
+
.replace("Northwestern", "NW.")
|
308 |
+
.replace("and", "&")
|
309 |
+
.replace("California", "CA"))
|
310 |
+
)
|
311 |
+
}
|
312 |
+
if label is not None:
|
313 |
+
return transformations.get(x, (None, lambda lbl: lbl))[1](label)
|
314 |
+
|
315 |
+
return transformations.get(x, (f"datum.{x}", None))[0]
|
316 |
+
|
317 |
+
def get_hex(df, color, sort_order):
|
318 |
+
"""
|
319 |
+
Returns a list of hex color codes sorted based on `sort_order`.
|
320 |
+
"""
|
321 |
+
return list(df.drop_duplicates(subset=color, keep="first")
|
322 |
+
.set_index(color)
|
323 |
+
.reindex(sort_order)
|
324 |
+
.dropna()["color"])
|
325 |
+
|
326 |
+
|
327 |
+
def create_bar_chart(df, x, y, title, color=None, stacked=False, colors=None):
|
328 |
+
"""
|
329 |
+
Generalized function to create a bar chart, supporting both standard and stacked bars.
|
330 |
+
"""
|
331 |
+
# helper functions
|
332 |
+
sort, angle, height, y_title = get_chart_settings(x,stacked)
|
333 |
+
label_transform = get_label_transform(x)
|
334 |
+
|
335 |
+
# create base chart
|
336 |
+
chart = (
|
337 |
+
alt.Chart(df)
|
338 |
+
.mark_bar(stroke="black", strokeWidth=0.1)
|
339 |
+
.transform_calculate(xlabel=label_transform)
|
340 |
+
.encode(
|
341 |
+
x=alt.X("xlabel:N", sort=sort,
|
342 |
+
axis=alt.Axis(labelAngle=angle, title=None, labelLimit=200)),
|
343 |
+
y=alt.Y(y, axis=alt.Axis(title=y_title, offset = -5)),
|
344 |
+
tooltip=[alt.Tooltip(x, type="nominal"), alt.Tooltip(y, type="quantitative")]
|
345 |
+
)
|
346 |
+
.properties(width="container", height=height)
|
347 |
+
|
348 |
+
)
|
349 |
+
|
350 |
+
if stacked:
|
351 |
+
# order stacks
|
352 |
+
sort_order = ["30x30-conserved", "other-conserved", "unknown", "non-conserved"]
|
353 |
+
color_hex = get_hex(df[[color, "color"]], color, sort_order)
|
354 |
+
sort_order = sort_order[:len(color_hex)]
|
355 |
+
df["stack_order"] = df[color].apply(lambda val: sort_order.index(val) if val in sort_order else len(sort_order))
|
356 |
+
|
357 |
+
# build chart
|
358 |
+
chart = chart.encode(
|
359 |
+
x=alt.X("xlabel:N", sort=sort, title=None, axis=alt.Axis(labels=False)),
|
360 |
+
y=alt.Y(y, axis=alt.Axis(title=y_title, offset = -5),scale = alt.Scale(domain = [0,1])),
|
361 |
+
|
362 |
+
color=alt.Color(color, sort=sort_order, scale=alt.Scale(domain=sort_order, range=color_hex)) ,
|
363 |
+
order=alt.Order("stack_order:Q", sort="ascending"),
|
364 |
+
tooltip=[
|
365 |
+
alt.Tooltip(x, type="nominal"),
|
366 |
+
alt.Tooltip(color, type="nominal"),
|
367 |
+
alt.Tooltip("percent_group", type="quantitative", format=",.1%"),
|
368 |
+
alt.Tooltip("acres", type="quantitative", format=",.0f"),
|
369 |
+
],
|
370 |
+
)
|
371 |
+
|
372 |
+
# use shorter label names (to save space)
|
373 |
+
labels_df = colors.copy()
|
374 |
+
labels_df["xlabel"] = [get_label_transform(x, str(lab)) for lab in colors[x]]
|
375 |
+
|
376 |
+
# create symbols/label below chart; dots match map colors.
|
377 |
+
symbol_layer = (
|
378 |
+
alt.Chart(labels_df)
|
379 |
+
.mark_point(filled=True, shape="circle", size=100, tooltip=False, yOffset=5)
|
380 |
+
.encode(
|
381 |
+
x=alt.X("xlabel:N", sort=sort,
|
382 |
+
axis=alt.Axis(labelAngle=angle, title=None, labelLimit=200)),
|
383 |
+
color=alt.Color("color:N", scale=None),
|
384 |
+
)
|
385 |
+
.properties(height=1, width="container")
|
386 |
+
)
|
387 |
+
|
388 |
+
# append symbols below base chart
|
389 |
+
final_chart = alt.vconcat(chart, symbol_layer, spacing=8).resolve_scale(x="shared")
|
390 |
+
|
391 |
+
|
392 |
+
else: #if not stacked, do single chart
|
393 |
+
final_chart = chart.encode(
|
394 |
+
color=alt.Color("color").scale(None)
|
395 |
+
)
|
396 |
+
|
397 |
+
# customize chart
|
398 |
+
final_chart = final_chart.properties(
|
399 |
+
title=title
|
400 |
+
).configure_legend(
|
401 |
+
symbolStrokeWidth=0.1, direction="horizontal", orient="top",
|
402 |
+
columns=2, title=None, labelOffset=2, offset=5,
|
403 |
+
symbolType="square", labelFontSize=13,
|
404 |
+
).configure_title(
|
405 |
+
fontSize=18, align="center", anchor="middle", offset = 10
|
406 |
+
)
|
407 |
+
|
408 |
+
return final_chart
|