import streamlit as st import streamlit.components.v1 as components import base64 import leafmap.maplibregl as leafmap import altair as alt import ibis from ibis import _ import ibis.selectors as s from typing import Optional def to_streamlit( self, width: Optional[int] = None, height: Optional[int] = 600, scrolling: Optional[bool] = False, **kwargs, ): try: import streamlit.components.v1 as components import base64 raw_html = self.to_html().encode("utf-8") raw_html = base64.b64encode(raw_html).decode() return components.iframe( f"data:text/html;base64,{raw_html}", width=width, height=height, scrolling=scrolling, **kwargs, ) except Exception as e: raise Exception(e) # ca_pmtiles = "https://huggingface.co/datasets/boettiger-lab/ca-30x30/resolve/main/ca2024-30m-tippe.pmtiles" # ca_parquet = "https://huggingface.co/datasets/boettiger-lab/ca-30x30/resolve/main/ca2024-30m.parquet" ca_pmtiles = "https://huggingface.co/datasets/boettiger-lab/ca-30x30/resolve/main/cpad-stats.pmtiles" ca_parquet = "https://huggingface.co/datasets/boettiger-lab/ca-30x30/resolve/main/cpad-stats.parquet" ca_area_acres = 1.014e8 #acres style_choice = "GAP Status Code" con = ibis.duckdb.connect(extensions=["spatial"]) ca = (con .read_parquet(ca_parquet) .cast({"geom": "geometry"}) .cast({"crop_expansion": "int64"}) ) private_access_color = "#DE881E" # orange public_access_color = "#3388ff" # blue tribal_color = "#BF40BF" # purple mixed_color = "#005a00" # green year2023_color = "#26542C" # green year2024_color = "#F3AB3D" # orange federal_color = "#529642" # green state_color = "#A1B03D" # light green local_color = "#365591" # blue special_color = "#529642" # brown private_color = "#7A3F1A" # brown joint_color = "#DAB0AE" # pink county_color = "#BFD76B" # green city_color = "#BDC368" #green hoa_color = "#A89BBC" # purple nonprofit_color = "#D77031" #orange from functools import reduce def get_summary(ca, combined_filter, column, colors=None): df = ca.filter(combined_filter) # if colors is not None and not colors.empty: # df used for chart. # df = df.filter(_.reGAP <3) # only show gaps 1 and 2 for chart. df = (df .group_by(*column) # unpack the list for grouping .aggregate(percent_protected=100 * _.Acres.sum() / ca_area_acres, mean_richness = (_.richness * _.Acres).sum() / _.Acres.sum(), mean_rsr = (_.rsr * _.Acres).sum() / _.Acres.sum(), all_rsr = (_.all_species_rwr * _.Acres).sum() / _.Acres.sum(), all_richness = (_.all_species_richness * _.Acres).sum() / _.Acres.sum(), irrecoverable_carbon = (_.irrecoverable_carbon * _.Acres).sum() / _.Acres.sum(), manageable_carbon = (_.manageable_carbon * _.Acres).sum() / _.Acres.sum(), carbon_lost = (_.deforest_carbon * _.Acres).sum() / _.Acres.sum(), human_impact = (_.human_impact * _.Acres).sum() / _.Acres.sum(), svi = (_.svi * _.Acres).sum() / _.Acres.sum(), svi_socioeconomic_status = (_.svi_socioeconomic_status * _.Acres).sum() / _.Acres.sum(), svi_household_char = (_.svi_household_char * _.Acres).sum() / _.Acres.sum(), svi_racial_ethnic_minority = (_.svi_racial_ethnic_minority * _.Acres).sum() / _.Acres.sum(), svi_housing_transit = (_.svi_housing_transit * _.Acres).sum() / _.Acres.sum(), # biodiversity_intactness_loss = (_.biodiversity_intactness_loss * _.Acres).sum() / _.Acres.sum(), # crop_reduction = (_.crop_reduction * _.Acres).sum() / _.Acres.sum(), # crop_expansion = (_.crop_expansion * _.Acres).sum() / _.Acres.sum(), # forest_loss = (_.forest_integrity_loss * _.Acres).sum() / _.Acres.sum(), ) .mutate(percent_protected=_.percent_protected.round(1)) ) if colors is not None and not colors.empty: # df = df.inner_join(colors, column) # chart colors df = df.cast({col: "string" for col in column}) df = df.to_pandas() return df def summary_table(column, colors, filter_cols, filter_vals,colorby_vals): filters = [] if filter_cols and filter_vals: #if a filter is selected, add to list of filters for filter_col, filter_val in zip(filter_cols, filter_vals): if len(filter_val) > 1: filters.append(getattr(_, filter_col).isin(filter_val)) else: filters.append(getattr(_, filter_col) == filter_val[0]) if column not in filter_cols: #show color_by variable in table filter_cols.append(column) filters.append(getattr(_, column).isin(colorby_vals[column])) combined_filter = reduce(lambda x, y: x & y, filters) df = get_summary(ca, combined_filter, [column], colors) # df used for charts df_tab = get_summary(ca, combined_filter, filter_cols, colors = None) #df used for printed table return df, df_tab def area_plot(df, column): base = alt.Chart(df).encode( alt.Theta("percent_protected:Q").stack(True), ) pie = ( base .mark_arc(innerRadius= 40, outerRadius=100) .encode(alt.Color("color:N").scale(None).legend(None), tooltip=['percent_protected', column]) ) text = ( base .mark_text(radius=80, size=14, color="white") .encode(text = column + ":N") ) plot = pie # pie + text return plot.properties(width="container", height=300) def get_pmtiles_style(paint, alpha, cols, values): filters = [] for col, val in zip(cols, values): filter_condition = ["match", ["get", col], val, True, False] filters.append(filter_condition) combined_filter = ["all"] + filters return { "version": 8, "sources": { "ca": { "type": "vector", "url": "pmtiles://" + ca_pmtiles, } }, "layers": [{ "id": "ca30x30", "source": "ca", "source-layer": "layer", # "source-layer": "ca202430m", # "source-layer": "ca2024", "type": "fill", "filter": combined_filter, # Use the combined filter "paint": { "fill-color": paint, "fill-opacity": alpha } }] } def bar_chart(df, x, y): chart = alt.Chart(df).mark_bar().encode( x=x, y=y, color=alt.Color('color').scale(None) ).properties(width="container", height=300) return chart def getButtons(style_options, style_choice, default_gap=None): column = style_options[style_choice]['property'] opts = [style[0] for style in style_options[style_choice]['stops']] default_gap = default_gap or {} buttons = { name: st.checkbox(f"{name}", value=default_gap.get(name, False), key=column + str(name)) for name in opts } filter_choice = [key for key, value in buttons.items() if value] # return only selected d = {} d[column] = filter_choice return d default_gap = { 1: True, 2: True, } def getColorVals(style_options, style_choice): #adding "color by" values to table column = style_options[style_choice]['property'] opts = [style[0] for style in style_options[style_choice]['stops']] d = {} d[column] = opts return d manager = { 'property': 'manager_type', 'type': 'categorical', 'stops': [ ['Federal', federal_color], ['State', state_color], ['Non Profit', nonprofit_color], ['Special District', special_color], ['Unknown', "grey"], ['County', county_color], ['City', city_color], ['Joint', joint_color], ['Tribal', tribal_color], ['Private', private_color], ['Home Owners Association', hoa_color] ] } easement = { 'property': 'Easement', 'type': 'categorical', 'stops': [ [0, public_access_color], [1, private_access_color] ] } year = { 'property': 'established', 'type': 'categorical', 'stops': [ [2023, year2023_color], [2024, year2024_color] ] } access = { 'property': 'access_type', 'type': 'categorical', 'stops': [ ['Open Access', public_access_color], ['No Public Access', private_access_color], ['Unknown Access', "grey"], ['Restricted Access', tribal_color] ] } gap = { 'property': 'reGAP', 'type': 'categorical', 'stops': [ [1, "#26633d"], [2, "#879647"], [3, "#BBBBBB"], [4, "#F8F8F8"] ] } # area_type = { # 'property': 'type', # 'type': 'categorical', # 'stops': [ # ["Land", "green"], # ["Water", "blue"] # ] # } style_options = { "Year": year, "GAP Status Code": gap, "Manager Type": manager, "Easement": easement, "Public Access": access, # "Type": area_type } justice40 = "https://data.source.coop/cboettig/justice40/disadvantaged-communities.pmtiles" justice40_fill = { 'property': 'Disadvan', 'type': 'categorical', 'stops': [ [0, "rgba(255, 255, 255, 0)"], [1, "rgba(0, 0, 139, 1)"]]} justice40_style = { "version": 8, "sources": { "source1": { "type": "vector", "url": "pmtiles://" + justice40, "attribution": "Justice40"} }, "layers": [{ "id": "layer1", "source": "source1", "source-layer": "DisadvantagedCommunitiesCEJST", "filter": ["match", ["get", "StateName"], "California", True, False], "type": "fill", "paint": {"fill-color": justice40_fill, "fill-opacity": 0.6}}] } sv_pmtiles = "https://data.source.coop/cboettig/social-vulnerability/svi2020_us_county.pmtiles" def get_sv_style(column): return { "layers": [ { "id": "SVI", "source": "Social Vulnerability Index", "source-layer": "SVI2020_US_county", "filter": ["match", ["get", "STATE"], "California", True, False], "type": "fill", "paint": { "fill-color": ["interpolate", ["linear"], ["get", column], 0, "#FFE6EE", 1, "#850101"] } } ] } st.set_page_config(layout="wide", page_title="CA Protected Areas Explorer", page_icon=":globe:") ''' # CA 30X30 Prototype ''' m = leafmap.Map(style="positron") filters = {} with st.sidebar: color_choice = st.radio("Color by:", style_options) colorby_vals = getColorVals(style_options, color_choice) alpha = st.slider("transparency", 0.0, 1.0, 0.5) st.divider() "Filters:" for label in style_options: with st.expander(label): if label == "GAP Status Code": # gap code 1 and 2 are on by default opts = getButtons(style_options, label, default_gap) else: opts = getButtons(style_options, label) filters.update(opts) selected = {k: v for k, v in filters.items() if v} #get selected filters if selected: filter_cols = list(selected.keys()) filter_vals = list(selected.values()) else: filter_cols = [] filter_vals = [] style = get_pmtiles_style(style_options[color_choice], alpha, filter_cols, filter_vals) m.add_pmtiles(ca_pmtiles, style=style, visible=True, name="CA", opacity=alpha, tooltip=True) st.divider() "Data Layers:" with st.expander("🦜 Biodiversity"): alpha_bio = st.slider("transparency", 0.0, 1.0, 0.4, key = "biodiversity") show_richness = st.toggle("Species Richness", False) if show_richness: m.add_tile_layer( url = "https://huggingface.co/datasets/boettiger-lab/ca-30x30/resolve/main/species-richness-ca/{z}/{x}/{y}.png", name="MOBI Species Richness", opacity=alpha_bio ) show_rsr = st.toggle("Range-Size Rarity") if show_rsr: m.add_tile_layer( url="https://huggingface.co/datasets/boettiger-lab/ca-30x30/resolve/main/range-size-rarity/{z}/{x}/{y}.png", name="MOBI Range-Size Rarity", opacity=alpha_bio) with st.expander("⛅ Carbon & Climate"): alpha_climate = st.slider("transparency", 0.0, 1.0, 0.3, key = "climate") show_irrecoverable_carbon = st.toggle("Irrecoverable Carbon") if show_irrecoverable_carbon: m.add_cog_layer("https://huggingface.co/datasets/boettiger-lab/ca-30x30/resolve/main/ca_irrecoverable_c_2018_cog.tif", palette="reds", name="Irrecoverable Carbon", transparent_bg=True, opacity = alpha_climate, fit_bounds=False, bidx=[1]) show_manageable_carbon = st.toggle("Manageable Carbon") if show_manageable_carbon: m.add_cog_layer("https://huggingface.co/datasets/boettiger-lab/ca-30x30/resolve/main/ca_manageable_c_2018_cog.tif", palette="purples", name="Manageable Carbon", transparent_bg=True, opacity = alpha_climate, fit_bounds=False) with st.expander("Climate and Economic Justice"): alpha_justice40 = st.slider("transparency", 0.0, 1.0, 0.3, key = "social justice") show_justice40 = st.toggle("Justice40") if show_justice40: m.add_pmtiles(justice40, style=justice40_style, visible=True, name="Justice40", opacity=alpha_justice40, tooltip=False, fit_bounds = False) with st.expander("Social Vulnerability"): alpha_justice = st.slider("transparency", 0.0, 1.0, 0.3, key = "SVI") show_sv = st.toggle("Social Vulnerability Index (SVI)") if show_sv: m.add_pmtiles(sv_pmtiles, style = get_sv_style("RPL_THEMES") ,visible=True, opacity=alpha_justice, tooltip=False, fit_bounds = False) show_sv_socio = st.toggle("Socioeconomic Status") if show_sv_socio: m.add_pmtiles(sv_pmtiles, style = get_sv_style("RPL_THEME1") ,visible=True, opacity=alpha_justice, tooltip=False, fit_bounds = False) show_sv_household = st.toggle("Household Characteristics") if show_sv_household: m.add_pmtiles(sv_pmtiles, style = get_sv_style("RPL_THEME2") ,visible=True, opacity=alpha_justice, tooltip=False, fit_bounds = False) show_sv_minority = st.toggle("Racial & Ethnic Minority Status") if show_sv_minority: m.add_pmtiles(sv_pmtiles, style = get_sv_style("RPL_THEME3") ,visible=True, opacity=alpha_justice, tooltip=False, fit_bounds = False) show_sv_housing = st.toggle("Housing Type & Transportation") if show_sv_housing: m.add_pmtiles(sv_pmtiles, style = get_sv_style("RPL_THEME4") ,visible=True, opacity=alpha_justice, tooltip=False, fit_bounds = False) with st.expander("🚜 Human Impacts"): alpha_hi = st.slider("transparency", 0.0, 1.0, 0.5, key = "hi") show_carbon_lost = st.toggle("Carbon Lost (2002-2022)") if show_carbon_lost: m.add_tile_layer( url="https://huggingface.co/datasets/boettiger-lab/ca-30x30/resolve/main/deforest-carbon-ca/{z}/{x}/{y}.png", name="Carbon Lost (2002-2022)", opacity = alpha_hi) show_human_impact = st.toggle("Human Impact") if show_human_impact: m.add_cog_layer( url = "https://huggingface.co/datasets/boettiger-lab/ca-30x30/resolve/main/ca_human_impact_cog.tif", name="Human Impact", transparent_bg=True, opacity = alpha_hi, fit_bounds=False) select_column = { "Year": "established", "GAP Status Code": "reGAP", "Manager Type": "manager_type", "Easement": "Easement", "Public Access": "access_type", # "Type": "type", } column = select_column[color_choice] select_colors = { "Year": year["stops"], "GAP Status Code": gap["stops"], "Manager Type": manager["stops"], "Easement": easement["stops"], "Public Access": access["stops"], # "Type": area_type["stops"] } colors = (ibis .memtable(select_colors[color_choice], columns = [column, "color"]) .to_pandas() ) main = st.container() df,df_tab = summary_table(column, colors, filter_cols, filter_vals, colorby_vals) with main: map_col, stats_col = st.columns([2,1]) with map_col: to_streamlit(m, height=700) st.dataframe(df_tab, use_container_width = True) svi = bar_chart(df, column, 'svi') svi_socioeconomic_status = bar_chart(df, column, 'svi_socioeconomic_status') svi_household_char = bar_chart(df, column, 'svi_household_char') svi_racial_ethnic_minority = bar_chart(df, column, 'svi_racial_ethnic_minority') svi_housing_transit = bar_chart(df, column, 'svi_housing_transit') richness_chart = bar_chart(df, column, 'mean_richness') rsr_chart = bar_chart(df, column, 'mean_rsr') # all_rsr = bar_chart(df, column, 'all_rsr') # all_richness = bar_chart(df, column, 'all_richness') irrecoverable_carbon = bar_chart(df, column, 'irrecoverable_carbon') manageable_carbon = bar_chart(df, column, 'manageable_carbon') carbon_lost = bar_chart(df, column, 'carbon_lost') human_impact = bar_chart(df, column, 'human_impact') # crop_expansion = bar_chart(df, column, 'crop_expansion') # biodiversity_intactness_loss = bar_chart(df, column, biodiversity_intactness_loss') # crop_reduction = bar_chart(df, column, 'crop_reduction') # forest_loss = bar_chart(df, column, 'forest_loss') total_percent = df.percent_protected.sum().round(1) with stats_col: with st.container(): f"{total_percent}% CA Covered" st.altair_chart(area_plot(df, column), use_container_width=True) with st.container(): if show_richness: "Species Richness" st.altair_chart(richness_chart, use_container_width=True) if show_rsr: "Range-Size Rarity" st.altair_chart(rsr_chart, use_container_width=True) if show_irrecoverable_carbon: "Irrecoverable Carbon" st.altair_chart(irrecoverable_carbon, use_container_width=True) if show_manageable_carbon: "Manageable Carbon" st.altair_chart(manageable_carbon, use_container_width=True) if show_sv: "Social Vulnerability Index" st.altair_chart(svi, use_container_width=True) if show_sv_socio: "SVI - Socioeconomic Status" st.altair_chart(svi_socioeconomic_status, use_container_width=True) if show_sv_household: "SVI - Household Characteristics" st.altair_chart(svi_household_char, use_container_width=True) if show_sv_minority: "SVI - Racial and Ethnic Minority" st.altair_chart(svi_racial_ethnic_minority, use_container_width=True) if show_sv_housing: "SVI - Housing Type and Transit" st.altair_chart(svi_housing_transit, use_container_width=True) if show_carbon_lost: "Carbon Lost ('02-'22)" st.altair_chart(carbon_lost, use_container_width=True) if show_human_impact: "Human Impact" st.altair_chart(human_impact, use_container_width=True) st.divider() footer = st.container() ''' ## Credits Authors: Cassie Buhler & Carl Boettiger, UC Berkeley License: BSD-2-clause ### Data sources - California Protected Areas Database by CA Nature. Data: https://www.californianature.ca.gov/datasets/CAnature::30x30-conserved-areas-terrestrial-2024/about. License: Public Domain - Climate and Economic Justice Screening Tool, US Council on Environmental Quality, Justice40, data: https://beta.source.coop/repositories/cboettig/justice40/description/, License: Public Domain - CDC 2020 Social Vulnerability Index by US Census Track. Data: https://source.coop/repositories/cboettig/social-vulnerability/description. License: Public Domain - Imperiled Species Richness and Range-Size-Rarity from NatureServe (2022). Data: https://beta.source.coop/repositories/cboettig/mobi. License CC-BY-NC-ND - Carbon-loss and farming impact by Vizzuality, on https://beta.source.coop/repositories/vizzuality/lg-land-carbon-data. Citation: https://doi.org/10.1101/2023.11.01.565036, License: CC-BY - Human Footprint by Vizzuality, on https://beta.source.coop/repositories/vizzuality/hfp-100. Citation: https://doi.org/10.3389/frsen.2023.1130896, License: Public Domain - Irrecoverable Carbon from Conservation International, reprocessed to COG on https://beta.source.coop/cboettig/carbon, citation: https://doi.org/10.1038/s41893-021-00803-6, License: CC-BY-NC '''