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
|