Laws Revegetation Data Analysis

Inyo County, California

Published

September 3, 2025

Modified

September 3, 2025

Background

The Laws Revegetation Project is based on the Laws Negative Declaration and the Laws Revegetation Plan describing 193 acres that would be reclassified from Type E to Type A. The reclassified land was to be revegetated and not irrigated in the future. This data report focuses on the revegetation targets for the Laws vegetation parcels: LAW090, LAW094, LAW095, LAW118/LAW129 that were part of this reclassification effort. data originate from 2022-2025.

A forthcoming amendment is being developed by the Inyo/LA Technical Group detailing any changes to the 2003 plan needed going forward. This analysis applies species-specific filtering according to the 2003 plan and policy-based capping rules recommended by the working group to assess revegetation targets.

Revegetation Targets:

  1. Perennial Cover ≥ 10%: Parcel-average native perennial cover from list of allowed species, with specific capping implemented for ATTO, ERNA10 and ATPO.
  2. Species with ≥3 Hits ≥ 6: At least six perennial species with 3+ hits - from list of allowed species plus ATTO, ERNA10 and ATPO.
  3. Species Richness ≥ 10: At least 10 distinct perennial species from list of allowed species plus ATTO, ERNA10 and ATPO
  4. Transect Cover ≥ 2%: Each transect must have ≥2% perennial cover from list of allowed species plus ATTO, ERNA10 and ATPO
  5. Grass Species Present: At least one grass species per parcel from list of allowed species

Capping Rules: - LAW090/094/095: ATTO and ERNA10 capped at 0.3% maximum contribution per parcel LAW090 Exception: No limit on ATPO species for LAW090 parcel LAW118/129: ATPO capped at 3%, ATTO and ERNA10 capped at 2% each

Overall Target Achievement: Requires ALL targets to be met (cover AND richness AND transect coverage AND grass presence)

Legend: ✓ = Target attained | ✗ = Target not attained

Detailed Methodology

Target 1: Perennial Cover ≥ 10% - Calculation: Sum hits for all allowable species per transect, convert to percentage cover - Formula: (hits / 200) × 100 per transect, then average across parcel - Capping: Apply species-specific limits before final calculation - Threshold: Final cover must be ≥ 10%

Target 2: Species with ≥3 Hits ≥ 6 - Method: Count species with 3+ hits from allowed species list - Process: Identify species meeting threshold, count total - Threshold: Must have at least 6 species with 3+ hits

Target 3: Species Richness ≥ 10 - Method: Count distinct perennial species from allowed species list - Process: Count unique species present in parcel - Threshold: Must have at least 10 distinct species

Target 4: Transect Cover ≥ 2% - Method: Calculate cover for each individual transect - Process: Ensure each transect meets 2% threshold - Assessment: Calculate compliance rate as percentage of transects meeting threshold

Target 5: Grass Species Present - Method: Verify at least one grass species per parcel - Process: Check for grass species from allowed species list - Assessment: Mark as compliant if any grass species found

Location

Parcels and transect locations are shown in the map below. Click on transects for details and use the layer control to toggle different features.

Interactive map showing Laws revegetation parcels and transect locations. Click on transects for details and use the layer control to toggle different features.

Data Processing

Step 1: Load and Combine Data Sources

LAW090/094/095 Parcels (2022-2025) - Load from LADWP Excel file covering 2022-2025 - Species hits zeroed if not on allowed species list - Cover conversion: cover = hits × (1/200) × 100 - Raw hits converted to percentage cover for analysis

LAW118/129 Parcels (2022-2025) - Load 2025 data from ICWD staff CSV with special parsing - Combine LAW118 and LAW129 into single “LAW118/129” parcel - Apply same cover conversion formula as other parcels - Maintain consistent naming conventions across all years

Data Integration - Apply uniform naming conventions and cover calculations - Ensure consistent data structure across all parcel-year combinations - Validate data completeness and quality

Code
# Load ALL data from LawsRevegetationData Excel file (Species List Data sheet)
# This file contains LAW090, LAW094, LAW095 for all years (2022-2025)
data_law909495 <- read_excel("data/LawsRevegetationData_SummaryTable_2025_ForICWD092525.xlsx", 
                             sheet = "Species List Data") %>%
  clean_names() %>%
  select(parcel, year, transect, species, cover) %>%
  rename(hits = cover) %>%  # Rename cover to hits for consistency
  mutate(
    year = as.numeric(year),
    transect = as.character(transect),  # Ensure transect is character
    species = str_trim(species)  # Remove leading/trailing spaces from species names
  ) %>%
  filter(year >= 2022 & year <= 2025) 
# %>%
  # filter(!str_detect(transect, "999"))

# Load LAW118/129 2025 data from CSV (different format)
# This file has a special format with metadata in first 3 rows
# Read without headers to preserve the raw structure
law118_129_raw <- read_csv("data/raw/reveg/LAW118_129_reveg2025_e.csv", 
                          col_names = FALSE,
                          show_col_types = FALSE)

# Extract metadata from first 3 rows
# Row 1: Parcel names (LAW118, LAW129)
# Row 2: Transect numbers  
# Row 3: Bearing numbers
parcel_names <- as.character(law118_129_raw[1, -1])  # Skip first column 
transect_numbers <- as.character(law118_129_raw[2, -1])
bearing_numbers <- as.numeric(law118_129_raw[3, -1])

# Data processing summary for LAW118/129
cat("LAW118/129 data processing:\n",
    "- Raw data dimensions:", nrow(law118_129_raw), "x", ncol(law118_129_raw), "\n",
    "- Parcel names:", paste(unique(parcel_names), collapse = ", "), "\n",
    "- Total transects (LAW118 + LAW129):", length(transect_numbers), "\n",
    "- Unique transect numbers:", paste(unique(transect_numbers), collapse = ", "), "\n",
    "- Note: Transect numbers overlap between LAW118 and LAW129, will create unique identifiers\n")
LAW118/129 data processing:
 - Raw data dimensions: 10 x 21 
 - Parcel names: LAW118, LAW129 
 - Total transects (LAW118 + LAW129): 20 
 - Unique transect numbers: 6, 8, 10, 11, 13, 14, 1, 2, 3, 5, 7, 12, 15, 17, 21 
 - Note: Transect numbers overlap between LAW118 and LAW129, will create unique identifiers
Code
# Process species data (rows 4+)
species_data <- law118_129_raw[4:nrow(law118_129_raw), ]

# Create the long format data for LAW118/129 manually
law118_129_processed <- data.frame()

# Loop through each species (rows 4+)
for (i in 1:nrow(species_data)) {
  species_name <- as.character(species_data[i, 1])
  
  # Loop through each transect (columns 2+)
  for (j in 2:ncol(species_data)) {
    hits_value <- as.numeric(species_data[i, j])
    
    if (!is.na(hits_value) && hits_value > 0) {
      # Create a row for this species-transect combination
      # Combine LAW118 and LAW129 into single "LAW118/129" parcel
      parcel_name <- ifelse(parcel_names[j-1] %in% c("LAW118", "LAW129"), "LAW118/129", parcel_names[j-1])
      
      # Create unique transect identifier by combining parcel and transect number
      unique_transect <- paste0(parcel_names[j-1], "_", transect_numbers[j-1])
      
      new_row <- data.frame(
        parcel = parcel_name,
        year = 2025,
        transect = unique_transect,
        species = str_trim(species_name),
        hits = hits_value
      )
      
      law118_129_processed <- bind_rows(law118_129_processed, new_row)
    }
  }
}

# Combine all data
data_combined <- bind_rows(data_law909495, law118_129_processed)

# Fix parcel naming for consistency - combine LAW118 and LAW129 into "LAW118/129"
data_combined <- data_combined %>%
  mutate(
    parcel = case_when(
      parcel == "LAW129" & year == 2022 ~ "LAW118/129",
      parcel == "LAW118" & year == 2023 ~ "LAW118/129",  # Add LAW118 2023 to LAW118/129
      TRUE ~ parcel
    )
  )

# Display data processing summary
cat("Data processing summary:\n",
    "- LAW090/094/095 data:", nrow(data_law909495), "rows\n",
    "- LAW118/129 2025 data:", nrow(law118_129_processed), "rows\n",
    "- Combined data:", nrow(data_combined), "rows\n",
    "- Unique parcels:", paste(unique(data_combined$parcel), collapse = ", "), "\n",
    "- Years covered:", paste(sort(unique(data_combined$year)), collapse = ", "), "\n")
Data processing summary:
 - LAW090/094/095 data: 826 rows
 - LAW118/129 2025 data: 55 rows
 - Combined data: 881 rows
 - Unique parcels: LAW090, LAW094, LAW095, LAW118/129 
 - Years covered: 2022, 2023, 2024, 2025 
Code
# Visual audit: Show sample of LAW090/094/095 data
cat("\n=== LAW090/094/095 Data Sample ===\n")

=== LAW090/094/095 Data Sample ===
Code
print(head(data_law909495, 10))
# A tibble: 10 × 5
   parcel  year transect species  hits
   <chr>  <dbl> <chr>    <chr>   <dbl>
 1 LAW090  2022 61       ATPO        3
 2 LAW090  2022 61       SATR12      0
 3 LAW090  2022 61       ATCA2       2
 4 LAW090  2022 61       ATTO       11
 5 LAW090  2022 61       SAVE4       6
 6 LAW090  2022 61       POFR        0
 7 LAW090  2022 61       SPAI        0
 8 LAW090  2022 62       ERNA10      9
 9 LAW090  2022 62       ATPO       10
10 LAW090  2022 62       SATR12      0
Code
# Visual audit: Show sample of LAW118/129 data
cat("\n=== LAW118/129 2025 Data Sample ===\n")

=== LAW118/129 2025 Data Sample ===
Code
print(head(law118_129_processed, 10))
       parcel year  transect species hits
1  LAW118/129 2025  LAW118_6  ERNA10    8
2  LAW118/129 2025  LAW118_8  ERNA10   16
3  LAW118/129 2025 LAW118_13  ERNA10    1
4  LAW118/129 2025  LAW129_1  ERNA10   38
5  LAW118/129 2025  LAW129_2  ERNA10   82
6  LAW118/129 2025  LAW129_3  ERNA10   45
7  LAW118/129 2025  LAW129_5  ERNA10   72
8  LAW118/129 2025  LAW129_6  ERNA10    6
9  LAW118/129 2025  LAW129_7  ERNA10    4
10 LAW118/129 2025  LAW129_8  ERNA10   54
Code
# Visual audit: Show combined data summary
cat("\n=== Combined Data Summary ===\n")

=== Combined Data Summary ===
Code
data_combined %>%
  group_by(parcel, year) %>%
  summarise(
    n_transects = n_distinct(transect),
    n_species = n_distinct(species),
    total_hits = sum(hits),
    .groups = 'drop'
  ) %>%
  print()
# A tibble: 12 × 5
   parcel      year n_transects n_species total_hits
   <chr>      <dbl>       <int>     <int>      <dbl>
 1 LAW090      2022          20        15        608
 2 LAW090      2024          20        10        523
 3 LAW090      2025          30         8        749
 4 LAW094      2022          20        11        401
 5 LAW094      2024          20         7        386
 6 LAW094      2025          20        10        339
 7 LAW095      2022          20        12        440
 8 LAW095      2024          20         8        446
 9 LAW095      2025          20         8        297
10 LAW118/129  2022          20        13        648
11 LAW118/129  2023          15        16        682
12 LAW118/129  2025          20         7        985
Code
# Note about LAW118/129 transect counting
cat("\nNote: LAW118/129 transects use unique identifiers (e.g., LAW118_6, LAW129_6) to handle overlapping transect numbers between parcels.\n")

Note: LAW118/129 transects use unique identifiers (e.g., LAW118_6, LAW129_6) to handle overlapping transect numbers between parcels.
Code
# Visual audit: Show hit-to-cover conversion
cat("\n=== Hit-to-Cover Conversion Example ===\n")

=== Hit-to-Cover Conversion Example ===
Code
# Show sample data with hit-to-cover conversion
# cover_percent is the species cover at the transect level (hits/200*100)
sample_data <- data_combined %>%
  filter(parcel == "LAW090", year == 2025) %>%
  head(10) %>%
  mutate(
    cover_percent = hits / 200 * 100  # Convert hits to cover percentage at transect level
  )
print(sample_data, width = Inf)
# A tibble: 10 × 6
   parcel  year transect species  hits cover_percent
   <chr>  <dbl> <chr>    <chr>   <dbl>         <dbl>
 1 LAW090  2025 61       SAVE4      11           5.5
 2 LAW090  2025 61       ATPO        7           3.5
 3 LAW090  2025 61       ATTO       10           5  
 4 LAW090  2025 62       ATTO       31          15.5
 5 LAW090  2025 62       ATCA2       5           2.5
 6 LAW090  2025 62       ERNA10      1           0.5
 7 LAW090  2025 63       ATCA2       4           2  
 8 LAW090  2025 64       SATR12      0           0  
 9 LAW090  2025 64       ATCA2      13           6.5
10 LAW090  2025 64       ERNA10      1           0.5
Code
# Visual audit: Show transect-to-parcel averaging
cat("\n=== Transect-to-Parcel Averaging Example ===\n")

=== Transect-to-Parcel Averaging Example ===
Code
# Show how transect-level cover is averaged to parcel-level cover
# First, calculate transect-level total cover
transect_totals <- data_combined %>%
  filter(parcel == "LAW090", year == 2025) %>%
  group_by(transect) %>%
  summarise(
    transect_total_hits = sum(hits),
    transect_cover_percent = sum(hits) / 200 * 100,
    .groups = 'drop'
  )

# Calculate parcel-level average (mean of all transect cover percentages)
parcel_avg <- mean(transect_totals$transect_cover_percent)
n_transects <- nrow(transect_totals)

# Add parcel-level statistics to each transect row for display
parcel_averaging_example <- transect_totals %>%
  mutate(
    n_transects = n_transects,
    parcel_average_cover = parcel_avg
  ) %>%
  select(transect, transect_total_hits, transect_cover_percent, n_transects, parcel_average_cover)

# Print with all columns visible (width = Inf ensures all columns are shown)
print(parcel_averaging_example, width = Inf, n = Inf)
# A tibble: 30 × 5
   transect transect_total_hits transect_cover_percent n_transects
   <chr>                  <dbl>                  <dbl>       <int>
 1 1                         28                   14            30
 2 10                        20                   10            30
 3 2                         12                    6            30
 4 3                         16                    8            30
 5 4                         35                   17.5          30
 6 5                         36                   18            30
 7 6                         33                   16.5          30
 8 61                        28                   14            30
 9 62                        37                   18.5          30
10 63                         4                    2            30
11 64                        14                    7            30
12 65                        23                   11.5          30
13 66                        16                    8            30
14 67                        24                   12            30
15 68                        17                    8.5          30
16 69                        10                    5            30
17 7                         19                    9.5          30
18 70                        17                    8.5          30
19 71                        11                    5.5          30
20 72                        21                   10.5          30
21 73                        44                   22            30
22 74                        35                   17.5          30
23 75                        34                   17            30
24 76                        29                   14.5          30
25 77                        29                   14.5          30
26 78                        32                   16            30
27 79                        14                    7            30
28 8                         17                    8.5          30
29 80                        36                   18            30
30 9                         58                   29            30
   parcel_average_cover
                  <dbl>
 1                 12.5
 2                 12.5
 3                 12.5
 4                 12.5
 5                 12.5
 6                 12.5
 7                 12.5
 8                 12.5
 9                 12.5
10                 12.5
11                 12.5
12                 12.5
13                 12.5
14                 12.5
15                 12.5
16                 12.5
17                 12.5
18                 12.5
19                 12.5
20                 12.5
21                 12.5
22                 12.5
23                 12.5
24                 12.5
25                 12.5
26                 12.5
27                 12.5
28                 12.5
29                 12.5
30                 12.5

Step 2: Create Parcel Groups for Analysis

Purpose: Group related parcels together for consistent analysis and rule application

LAW90_94_95 Group - Parcels: LAW090, LAW094, LAW095 - Use same species list and capping rules - Apply strict capping: ERNA10 and ATTO capped at 0.3% each - ATPO species have no limit for LAW090 parcel

LAW118_129 Group - Parcels: LAW118, LAW129 (treated as single management unit) - Use same species list and capping rules - Apply more generous capping: ERNA10/ATTO capped at 2% each, ATPO capped at 3%

Step 3: Apply Species Filtering and Capping Rules

Species Filtering Process 1. Load Allowable Species Lists: Import species lists for each parcel group 2. Filter Data: Keep only species that are on the allowable lists 3. Apply Capping Rules: Limit contribution of specific species (ERNA10, ATTO, ATPO) 4. Calculate Final Cover: Sum capped species contributions for compliance assessment

Capping Rules by Parcel Group - LAW90/94/95: ERNA10 and ATTO capped at 0.3% each, ATPO unlimited for LAW090 - LAW118/129: ERNA10/ATTO capped at 2% each, ATPO capped at 3%

Code
# Use all data without species filtering for now
data_filtered <- data_combined %>%
  mutate(
    parcel_group = case_when(
      parcel %in% c("LAW090", "LAW094", "LAW095") ~ "LAW90_94_95",
      parcel %in% c("LAW118", "LAW129") ~ "LAW118_129",
      TRUE ~ "Other"
    )
  )

# Debug: Check data loading
cat("Data filtered rows:", nrow(data_filtered), "\n")
Data filtered rows: 881 
Code
cat("Data filtered columns:", paste(colnames(data_filtered), collapse = ", "), "\n")
Data filtered columns: parcel, year, transect, species, hits, parcel_group 

Step 1.6:

Load Allowable Species Lists Purpose: Define which species are allowed for compliance calculations for each parcel group

Two Separate Lists: LAW90/94/95 and LAW118/129 have different allowable species ERNA10, ATTO, ATPO automatically added to both lists.

Code
# Load allowable species from BOTH sheets and combine
# Sheet 1: LAW90/94/95 species
allowable_law90_94_95 <- read_excel("data/TypeE_Transfer_SppList.xlsx", sheet = "TypeE_List_90_94_95") %>%
  clean_names() %>%
  pull(species_code) %>%
  c("ERNA10", "ATTO", "ATPO") %>%  # Add core species if not already included
  unique()

# Sheet 2: LAW118/129 species  
allowable_law118_129 <- read_excel("data/TypeE_Transfer_SppList.xlsx", sheet = "TypeE_List_118_129") %>%
  clean_names() %>%
  pull(species_code) %>%
  c("ERNA10", "ATTO", "ATPO") %>%  # Add core species if not already included
  unique()

# Debug: Check if SATR12 is in the loaded allowable species
cat("SATR12 in loaded LAW90/94/95:", "SATR12" %in% allowable_law90_94_95, "\n")
SATR12 in loaded LAW90/94/95: FALSE 
Code
cat("SATR12 in loaded LAW118/129:", "SATR12" %in% allowable_law118_129, "\n")
SATR12 in loaded LAW118/129: FALSE 
Code
# Create combined allowable species dataframe
allowable_species_combined <- data.frame(
  species = c(allowable_law90_94_95, allowable_law118_129),
  parcel_group = c(rep("LAW90_94_95", length(allowable_law90_94_95)),
                   rep("LAW118_129", length(allowable_law118_129)))
) %>%
  distinct()  # Remove any duplicates

# Debug: Show allowable species lists
cat("Allowable species summary:\n",
    "- LAW90/94/95 allowable species:", length(allowable_law90_94_95), "species\n",
    "- LAW118/129 allowable species:", length(allowable_law118_129), "species\n",
    "- Additional species (ERNA10, ATTO, ATPO) included in both lists\n")
Allowable species summary:
 - LAW90/94/95 allowable species: 27 species
 - LAW118/129 allowable species: 22 species
 - Additional species (ERNA10, ATTO, ATPO) included in both lists
Code
# Debug: Check data_filtered
cat("Data filtered rows:", nrow(data_filtered), "\n")
Data filtered rows: 881 
Code
cat("Data filtered columns:", paste(colnames(data_filtered), collapse = ", "), "\n")
Data filtered columns: parcel, year, transect, species, hits, parcel_group 
Code
# Calculate compliance metrics with error handling
compliance_metrics <- tryCatch({
  data_filtered %>%
    group_by(parcel, year) %>%
    summarise(
      species_richness = n_distinct(species),#need to add trace species also tho
      total_transects = n_distinct(transect),
      total_hits = if("hits" %in% colnames(data_filtered)) sum(hits, na.rm = TRUE) else 0,
      avg_cover_percent = if("hits" %in% colnames(data_filtered)) round(mean((hits / 200) * 100, na.rm = TRUE), 2) else 0,
      .groups = 'drop'
    ) %>%
    mutate(
      capped_cover_percent = ifelse(avg_cover_percent > 10, 10, avg_cover_percent),
      richness_goal = ifelse(species_richness >= 10, "✓", "✗"),
      cover_goal = ifelse(avg_cover_percent >= 10, "✓", "✗"),
      capped_cover_goal = ifelse(capped_cover_percent >= 10, "✓", "✗"),
      overall_compliance = ifelse(richness_goal == "✓" & cover_goal == "✓", "Yes", "No")
    )
}, error = function(e) {
  cat("Error in compliance_metrics calculation:", e$message, "\n")
  # Return empty data frame with correct structure
  data.frame(
    parcel = character(),
    year = numeric(),
    species_richness = numeric(),
    total_transects = numeric(),
    total_hits = numeric(),
    avg_cover_percent = numeric(),
    capped_cover_percent = numeric(),
    richness_goal = character(),
    cover_goal = character(),
    capped_cover_goal = character(),
    overall_compliance = character()
  )
})

Compliance Results

Comprehensive Parcel-Year Summary

Code
# Debug: Check if compliance_metrics exists and has data
cat("Compliance metrics exists:", exists("compliance_metrics"), "\n")
Compliance metrics exists: TRUE 
Code
if(exists("compliance_metrics")) {
  cat("Compliance metrics rows:", nrow(compliance_metrics), "\n")
  cat("Compliance metrics columns:", paste(colnames(compliance_metrics), collapse = ", "), "\n")
}
Compliance metrics rows: 12 
Compliance metrics columns: parcel, year, species_richness, total_transects, total_hits, avg_cover_percent, capped_cover_percent, richness_goal, cover_goal, capped_cover_goal, overall_compliance 

Results

Step 2: Calculate Metrics - Apply species capping rules (ATTO, ERNA10, ATPO limits) - Calculate cover percentages and species richness - Determine compliance status for each parcel-year combination - Remove duplicate entries to ensure data integrity

Code
# Function to calculate cover and capping for a parcel/year
calculate_parcel_cover <- function(parcel, year) {
  # Handle LAW118/129 as combined parcel
  if (parcel == "LAW118/129") {
    # Get data for LAW118/129 (already combined in data processing)
    parcel_data <- data_filtered %>%
      filter(parcel == "LAW118/129", year == !!year)
    # Get original data for transect counting
    original_parcel_data <- data_combined %>%
      filter(parcel == "LAW118/129", year == !!year)
  } else {
    # Get data for this specific parcel/year
    parcel_data <- data_filtered %>%
      filter(parcel == !!parcel, year == !!year)
    # Get original data for transect counting
    original_parcel_data <- data_combined %>%
      filter(parcel == !!parcel, year == !!year)
  }
  
  if (nrow(parcel_data) == 0) {
    return(NULL)
  }
  
  # Determine which species list to use
  allowable_species_codes <- if (parcel %in% c("LAW090", "LAW094", "LAW095")) {
    allowable_species_combined %>% 
      filter(parcel_group == "LAW90_94_95") %>% 
      pull(species)
  } else if (parcel == "LAW118/129") {
    allowable_species_combined %>% 
      filter(parcel_group == "LAW118_129") %>% 
      pull(species)
  } else {
    character(0)
  }
  
  # Filter to only allowable species
  parcel_allowable <- parcel_data %>%
    filter(species %in% allowable_species_codes)
  
  # Debug: Show species filtering for LAW118/129 2025
  if (parcel == "LAW118/129" && year == 2025) {
    cat("\n=== Species Filtering for", parcel, year, "===\n")
    cat("Allowable species codes:", paste(allowable_species_codes, collapse = ", "), "\n")
    cat("Species in data before filtering:", paste(unique(parcel_data$species), collapse = ", "), "\n")
    cat("Species after filtering:", paste(unique(parcel_allowable$species), collapse = ", "), "\n")
    excluded_species <- setdiff(unique(parcel_data$species), unique(parcel_allowable$species))
    if (length(excluded_species) > 0) {
      cat("EXCLUDED species (not allowable):", paste(excluded_species, collapse = ", "), "\n")
    }
  }
  
  # Calculate original cover (sum of all species cover)
  n_transects <- n_distinct(original_parcel_data$transect)
  original_cover <- if(nrow(parcel_allowable) > 0) {
    # Convert hits to cover: hits / 200 * 100
    total_cover_percent <- sum(parcel_allowable$hits / 200 * 100)
    parcel_average_cover <- total_cover_percent / n_transects
    
    # Debug: Show calculation for LAW118/129 2025
    if (parcel == "LAW118/129" && year == 2025) {
      cat("\n=== Cover Calculation Debug for", parcel, year, "===\n")
      cat("Number of transects:", n_transects, "\n")
      cat("Parcel average cover:", round(parcel_average_cover, 2), "%\n")
      cat("Species breakdown (before capping):\n")
      species_breakdown <- parcel_allowable %>% 
        group_by(species) %>% 
        summarise(total_hits = sum(hits), .groups = 'drop') %>%
        mutate(
          total_cover_percent = round(total_hits / 200 * 100, 2),
          species_avg_cover = round(total_cover_percent / n_transects, 2)
        ) %>%
        arrange(desc(species_avg_cover))
      print(species_breakdown)
    }
    
    parcel_average_cover
  } else {
    0
  }
  
  # Apply capping rules (from backup file)
  if(nrow(parcel_allowable) > 0) {
    parcel_species_cover <- parcel_allowable %>%
      group_by(species) %>%
      summarise(
        total_hits = sum(hits),
        .groups = 'drop'
      ) %>%
      mutate(
        species_cover = (total_hits / 200 * 100) / n_transects,
        # Apply species-specific capping rules
        capped_cover = case_when(
          # LAW90/94/95: Strict capping (0.3% max for ERNA10 and ATTO)
          parcel %in% c("LAW090", "LAW094", "LAW095") & species == "ERNA10" ~ pmin(species_cover, 0.3),
          parcel %in% c("LAW090", "LAW094", "LAW095") & species == "ATTO" ~ pmin(species_cover, 0.3),
          # LAW118/129: More generous capping (2% for ERNA10/ATTO, 3% for ATPO)
          parcel == "LAW118/129" & species == "ERNA10" ~ pmin(species_cover, 2.0),
          parcel == "LAW118/129" & species == "ATTO" ~ pmin(species_cover, 2.0),
          parcel == "LAW118/129" & species == "ATPO" ~ pmin(species_cover, 3.0),
          # All other species: No capping (full contribution)
          TRUE ~ species_cover
        )
      )
    
    
    # Calculate final cover after capping
    final_cover <- sum(parcel_species_cover$capped_cover)
    
    # Debug: Show capping results for LAW118/129 2025
    if (parcel == "LAW118/129" && year == 2025) {
      cat("\n=== Species Capping Results for", parcel, year, "===\n")
      capping_results <- parcel_species_cover %>%
        mutate(
          capping_applied = ifelse(species_cover != capped_cover, "YES", "NO"),
          reduction = round(species_cover - capped_cover, 2)
        ) %>%
        arrange(desc(capped_cover))
      print(capping_results)
      cat("Final cover after capping:", round(final_cover, 2), "%\n")
    }
  } else {
    final_cover <- 0
  }
  
  result <- data.frame(
    Parcel = parcel,
    Year = year,
    `Cover %` = round(original_cover, 2),
    `Transects` = n_transects,
    `Cover After Species Caps` = round(final_cover, 2),
    `Allowed Sp Count` = n_distinct(parcel_allowable$species),
    `Cover Status` = ifelse(final_cover >= 10, "✅ Compliant", "❌ Non-compliant")
  )
  
  return(result)
}

# Calculate for all parcels/years based on available data
# Get all unique parcel/year combinations from the data
available_combinations <- data_combined %>%
  select(parcel, year) %>%
  distinct() %>%
  arrange(parcel, year)

# Add LAW118/129 2025 data (combine LAW118 and LAW129)
law118_129_2025 <- calculate_parcel_cover("LAW118/129", 2025)

=== Species Filtering for LAW118/129 2025 ===
Allowable species codes: ACHY, ACSP12, ATCA2, ATCO, ATPA3, ATPO, EPNE, GRSP, KRLA2, LEFR2, MACA17, MESP2, PIDE4, PSARM, PSPO, SAVE4, STEPH, STPI, TEAX, TEGL, ERNA10, ATTO 
Species in data before filtering: ERNA10, ATPO, SATR12, ATCA2, COAR4, KRLA2, IVAX 
Species after filtering: ERNA10, ATPO, ATCA2, KRLA2 
EXCLUDED species (not allowable): SATR12, COAR4, IVAX 

=== Cover Calculation Debug for LAW118/129 2025 ===
Number of transects: 20 
Parcel average cover: 21.38 %
Species breakdown (before capping):
# A tibble: 4 × 4
  species total_hits total_cover_percent species_avg_cover
  <chr>        <dbl>               <dbl>             <dbl>
1 ERNA10         635               318.              15.9 
2 ATPO           139                69.5              3.48
3 ATCA2           77                38.5              1.93
4 KRLA2            4                 2                0.1 

=== Species Capping Results for LAW118/129 2025 ===
# A tibble: 4 × 6
  species total_hits species_cover capped_cover capping_applied reduction
  <chr>        <dbl>         <dbl>        <dbl> <chr>               <dbl>
1 ATPO           139          3.48         3    YES                  0.48
2 ERNA10         635         15.9          2    YES                 13.9 
3 ATCA2           77          1.92         1.92 NO                   0   
4 KRLA2            4          0.1          0.1  NO                   0   
Final cover after capping: 7.02 %
Code
# Create dashboard data for all available combinations
dashboard_data <- available_combinations %>%
  rowwise() %>%
  mutate(
    result = list(calculate_parcel_cover(parcel, year))
  ) %>%
  ungroup() %>%
  filter(!purrr::map_lgl(result, is.null)) %>%
  mutate(result = purrr::map(result, as.data.frame)) %>%
  pull(result) %>%
  bind_rows()

=== Species Filtering for LAW118/129 2025 ===
Allowable species codes: ACHY, ACSP12, ATCA2, ATCO, ATPA3, ATPO, EPNE, GRSP, KRLA2, LEFR2, MACA17, MESP2, PIDE4, PSARM, PSPO, SAVE4, STEPH, STPI, TEAX, TEGL, ERNA10, ATTO 
Species in data before filtering: ERNA10, ATPO, SATR12, ATCA2, COAR4, KRLA2, IVAX 
Species after filtering: ERNA10, ATPO, ATCA2, KRLA2 
EXCLUDED species (not allowable): SATR12, COAR4, IVAX 

=== Cover Calculation Debug for LAW118/129 2025 ===
Number of transects: 20 
Parcel average cover: 21.38 %
Species breakdown (before capping):
# A tibble: 4 × 4
  species total_hits total_cover_percent species_avg_cover
  <chr>        <dbl>               <dbl>             <dbl>
1 ERNA10         635               318.              15.9 
2 ATPO           139                69.5              3.48
3 ATCA2           77                38.5              1.93
4 KRLA2            4                 2                0.1 

=== Species Capping Results for LAW118/129 2025 ===
# A tibble: 4 × 6
  species total_hits species_cover capped_cover capping_applied reduction
  <chr>        <dbl>         <dbl>        <dbl> <chr>               <dbl>
1 ATPO           139          3.48         3    YES                  0.48
2 ERNA10         635         15.9          2    YES                 13.9 
3 ATCA2           77          1.92         1.92 NO                   0   
4 KRLA2            4          0.1          0.1  NO                   0   
Final cover after capping: 7.02 %
Code
# Add LAW118/129 2025 if it exists
if (!is.null(law118_129_2025)) {
  dashboard_data <- bind_rows(dashboard_data, law118_129_2025)
}

# Remove any duplicate rows
dashboard_data <- dashboard_data %>%
  distinct(Parcel, Year, .keep_all = TRUE)

# Visual audit: Show analytical results data
cat("\n=== Analytical Results Data ===\n")

=== Analytical Results Data ===
Code
cat("Total rows:", nrow(dashboard_data), "\n")
Total rows: 12 
Code
cat("Unique parcel-year combinations:", nrow(unique(dashboard_data[, c("Parcel", "Year")])), "\n")
Unique parcel-year combinations: 12 
Code
# Display wider table
if(nrow(dashboard_data) > 0) {
DT::datatable(
  dashboard_data,
    caption = 'Analytical Results Table',
    options = list(
      pageLength = -1,  # Show all rows by default
      scrollX = FALSE,  # Disable horizontal scrolling to make table wider
      dom = 'Bfrtip',
      buttons = c('copy', 'csv', 'excel', 'pdf', 'print'),
      columnDefs = list(
        list(className = 'dt-center', targets = c(1, 2, 3, 4, 5, 6)),
        list(width = '80px', targets = 0),  # Parcel column
        list(width = '60px', targets = 1),  # Year column
        list(width = '80px', targets = 2),  # Cover column
        list(width = '80px', targets = 3),  # Transects column
        list(width = '120px', targets = 4), # Cover After column
        list(width = '100px', targets = 5), # Allowed Sp Count column
        list(width = '120px', targets = 6) # Cover Status column
      ),
      extensions = 'Buttons',
      autoWidth = TRUE
    ),
    rownames = FALSE,
    width = '100%'
  )
} else {
  cat("No data available for display")
}

Species Richness Analysis

Step 3: Analyze Species Diversity - Combine transect-detected species with trace species observations - Filter to allowable species only - exclude non-allowable species from richness counts - Calculate total species richness per parcel for 2025 - Include common names for better species identification - Apply consistent LAW118/129 parcel naming

This section analyzes species richness for each parcel in 2025, including both transect-detected species and trace species detected on the parcel. Only allowable species are counted toward the richness targets.

Code
# Load trace species data
trace_species <- read_excel("data/LawsRevegetationData_SummaryTable_2025_ForICWD092525.xlsx", 
                           sheet = "Trace Species") %>%
  clean_names() %>%
  select(parcel, species = trace_species) %>%
  mutate(species = str_trim(species))

# Load species attributes for common names
species_attributes <- read_csv("data/species.csv", show_col_types = FALSE) %>%
  select(Code, CommonName) %>%
  rename(species = Code, common_name = CommonName)

# Get transect-detected species for 2025 (ALLOWABLE SPECIES ONLY)
transect_species_2025 <- data_combined %>%
  filter(year == 2025) %>%
  select(parcel, species) %>%
  distinct() %>%
  # Filter to only allowable species based on parcel group
  mutate(
    parcel_group = case_when(
      parcel %in% c("LAW090", "LAW094", "LAW095") ~ "LAW90_94_95",
      parcel %in% c("LAW118", "LAW129") ~ "LAW118_129",
      TRUE ~ "Other"
    )
  ) %>%
  left_join(allowable_species_combined, by = c("species", "parcel_group")) %>%
  filter(!is.na(parcel_group)) %>%  # Only keep allowable species
  filter(species %in% allowable_species_combined$species) %>%  # Double-check filtering
  select(parcel, species) %>%
  left_join(species_attributes, by = "species") %>%
  mutate(
    detection_method = "Transect",
    # Combine LAW118 and LAW129 into LAW118/129
    parcel = ifelse(parcel %in% c("LAW118", "LAW129"), "LAW118/129", parcel)
  )

# Get trace species for 2025 (ALLOWABLE SPECIES ONLY)
trace_species_2025 <- trace_species %>%
  filter(parcel %in% c("LAW090", "LAW094", "LAW095", "LAW118", "LAW129")) %>%
  # Filter to only allowable species based on parcel group
  mutate(
    parcel_group = case_when(
      parcel %in% c("LAW090", "LAW094", "LAW095") ~ "LAW90_94_95",
      parcel %in% c("LAW118", "LAW129") ~ "LAW118_129",
      TRUE ~ "Other"
    )
  ) %>%
  left_join(allowable_species_combined, by = c("species", "parcel_group")) %>%
  filter(!is.na(parcel_group)) %>%  # Only keep allowable species
  filter(species %in% allowable_species_combined$species) %>%  # Double-check filtering
  select(parcel, species) %>%
  left_join(species_attributes, by = "species") %>%
  mutate(
    detection_method = "Trace",
    # Combine LAW118 and LAW129 into LAW118/129
    parcel = ifelse(parcel %in% c("LAW118", "LAW129"), "LAW118/129", parcel)
  )

# Debug: Show which trace species are being filtered out
cat("\n=== Trace Species Filtering Debug ===\n")

=== Trace Species Filtering Debug ===
Code
trace_before_filter <- trace_species %>%
  filter(parcel %in% c("LAW090", "LAW094", "LAW095", "LAW118", "LAW129"))
cat("Trace species before filtering:", nrow(trace_before_filter), "rows\n")
Trace species before filtering: 34 rows
Code
cat("Trace species after filtering:", nrow(trace_species_2025), "rows\n")
Trace species after filtering: 31 rows
Code
cat("Filtered out:", nrow(trace_before_filter) - nrow(trace_species_2025), "non-allowable species\n")
Filtered out: 3 non-allowable species
Code
# Debug: Show which species are in the final trace species list
cat("Final trace species list:\n")
Final trace species list:
Code
print(unique(trace_species_2025$species))
 [1] "ACHY"   "ACSP12" "GRSP"   "AMDU2"  "PSPO"   "LEFR2"  "PSARM"  "ATCO"  
 [9] "MESP2"  "STEPH"  "SAVE4"  "KRLA2" 
Code
cat("SATR12 in trace species:", "SATR12" %in% trace_species_2025$species, "\n")
SATR12 in trace species: FALSE 
Code
# Debug: Check if SATR12 is in allowable species lists
cat("SATR12 in LAW90/94/95 allowable:", "SATR12" %in% allowable_law90_94_95, "\n")
SATR12 in LAW90/94/95 allowable: FALSE 
Code
cat("SATR12 in LAW118/129 allowable:", "SATR12" %in% allowable_law118_129, "\n")
SATR12 in LAW118/129 allowable: FALSE 
Code
cat("SATR12 in combined allowable:", "SATR12" %in% allowable_species_combined$species, "\n")
SATR12 in combined allowable: FALSE 
Code
# Combine all species detections
all_species_2025 <- bind_rows(
  transect_species_2025,
  trace_species_2025
) %>%
  # Use common name from species attributes, fallback to species code
  mutate(
    common_name = case_when(
      !is.na(common_name) & common_name != "" ~ common_name,
      TRUE ~ species
    )
  ) %>%
  select(parcel, species, common_name, detection_method) %>%
  distinct()

# Debug: Check if SATR12 is in the final combined species list
cat("SATR12 in final all_species_2025:", "SATR12" %in% all_species_2025$species, "\n")
SATR12 in final all_species_2025: FALSE 
Code
if("SATR12" %in% all_species_2025$species) {
  satr12_rows <- all_species_2025[all_species_2025$species == "SATR12", ]
  cat("SATR12 detection method:", paste(unique(satr12_rows$detection_method), collapse = ", "), "\n")
  cat("SATR12 parcels:", paste(unique(satr12_rows$parcel), collapse = ", "), "\n")
}

# Visual audit: Show species detection summary
cat("\n=== Species Detection Summary ===\n")

=== Species Detection Summary ===
Code
all_species_2025 %>%
  group_by(parcel, detection_method) %>%
  summarise(n_species = n(), .groups = 'drop') %>%
  print()
# A tibble: 8 × 3
  parcel     detection_method n_species
  <chr>      <chr>                <int>
1 LAW090     Trace                    7
2 LAW090     Transect                 6
3 LAW094     Trace                    9
4 LAW094     Transect                 7
5 LAW095     Trace                    7
6 LAW095     Transect                 5
7 LAW118/129 Trace                    1
8 LAW118/129 Transect                 4
Code
cat("\n=== Sample Species Detections ===\n")

=== Sample Species Detections ===
Code
print(head(all_species_2025, 15))
# A tibble: 15 × 4
   parcel species common_name               detection_method
   <chr>  <chr>   <chr>                     <chr>           
 1 LAW090 SAVE4   Greasewood                Transect        
 2 LAW090 ATPO    saltbush, Allscale        Transect        
 3 LAW090 ATTO    saltbush, Nevada/Torrey's Transect        
 4 LAW090 ATCA2   saltbush, Fourwing        Transect        
 5 LAW090 ERNA10  rabbitbrush, Common       Transect        
 6 LAW090 KRLA2   Winterfat                 Transect        
 7 LAW094 ATPO    saltbush, Allscale        Transect        
 8 LAW094 ATCA2   saltbush, Fourwing        Transect        
 9 LAW094 KRLA2   Winterfat                 Transect        
10 LAW094 LEFR2   alyssum, Desert           Transect        
11 LAW094 ERNA10  rabbitbrush, Common       Transect        
12 LAW094 ATTO    saltbush, Nevada/Torrey's Transect        
13 LAW094 AMDU2   Burrow-bush               Transect        
14 LAW095 ATCA2   saltbush, Fourwing        Transect        
15 LAW095 ATTO    saltbush, Nevada/Torrey's Transect        
Code
# Calculate species richness by parcel
species_richness_2025 <- all_species_2025 %>%
  group_by(parcel) %>%
  summarise(
    total_species = n_distinct(species),
    transect_species = n_distinct(species[detection_method == "Transect"]),
    trace_species = n_distinct(species[detection_method == "Trace"]),
    .groups = 'drop'
  ) %>%
  mutate(
    richness_goal_met = total_species >= 10,
    status = ifelse(richness_goal_met, "✅ ≥10 species", "❌ <10 species")
  )

# Create detailed species list for each parcel
species_lists_2025 <- all_species_2025 %>%
  group_by(parcel) %>%
  summarise(
    species_list = paste(common_name, collapse = ", "),
    .groups = 'drop'
  )

# Combine richness metrics with species lists
species_richness_table <- species_richness_2025 %>%
  left_join(species_lists_2025, by = "parcel") %>%
  select(parcel, total_species, transect_species, trace_species, status, species_list)

# Display species richness table
DT::datatable(
  species_richness_table,
  caption = 'Species Richness Analysis - 2025',
  options = list(
    pageLength = 10,
    scrollX = TRUE,
    dom = 'Bfrtip',
    buttons = c('copy', 'csv', 'excel', 'pdf', 'print'),
    columnDefs = list(
      list(className = 'dt-center', targets = c(1, 2, 3, 4)),
      list(width = '100px', targets = 0),  # Parcel column
      list(width = '80px', targets = 1),  # Total species
      list(width = '80px', targets = 2),  # Transect species
      list(width = '80px', targets = 3),  # Trace species
      list(width = '120px', targets = 4), # Status
      list(width = '300px', targets = 5)  # Species list
  ),
  extensions = 'Buttons',
    autoWidth = TRUE
  ),
  rownames = FALSE,
  width = '100%'
) %>%
  DT::formatStyle(
    'status',
    backgroundColor = DT::styleEqual(
      c("✅ ≥10 species", "❌ <10 species"),
      c("#d4edda", "#f8d7da")
    )
  )

Grass Species Analysis

Step 4: Assess Grass Species Presence - Join species richness data with species attributes to identify grass species - Check if each parcel has at least one grass species (Lifeform = ‘Grass’) - Create summary table showing grass species presence by parcel and year

Code
# Load species attributes for lifeform information
species_attributes_full <- read_csv("data/species.csv", show_col_types = FALSE) %>%
  select(Code, CommonName, Lifeform) %>%
  rename(species = Code, common_name = CommonName)

# Get all species detections for all years (not just 2025)
all_species_all_years <- data_combined %>%
  select(parcel, year, species) %>%
  distinct() %>%
  # Filter to only allowable species based on parcel group
  mutate(
    parcel_group = case_when(
      parcel %in% c("LAW090", "LAW094", "LAW095") ~ "LAW90_94_95",
      parcel %in% c("LAW118", "LAW129") ~ "LAW118_129",
      TRUE ~ "Other"
    )
  ) %>%
  left_join(allowable_species_combined, by = c("species", "parcel_group")) %>%
  filter(!is.na(parcel_group)) %>%  # Only keep allowable species
  filter(species %in% allowable_species_combined$species) %>%  # Double-check filtering
  select(parcel, year, species) %>%
  # Join with species attributes to get lifeform information
  left_join(species_attributes_full, by = "species") %>%
  # Apply consistent LAW118/129 naming
  mutate(
    parcel = ifelse(parcel %in% c("LAW118", "LAW129"), "LAW118/129", parcel)
  )

# Debug: Check what species are being detected and their lifeforms
cat("\n=== Grass Species Debug ===\n")

=== Grass Species Debug ===
Code
cat("Sample of species with lifeform data:\n")
Sample of species with lifeform data:
Code
print(head(all_species_all_years %>% select(species, Lifeform) %>% distinct(), 10))
# A tibble: 10 × 2
   species Lifeform
   <chr>   <chr>   
 1 ATPO    Shrub   
 2 ATCA2   Shrub   
 3 ATTO    Shrub   
 4 SAVE4   Shrub   
 5 ERNA10  Shrub   
 6 ACHY    Grass   
 7 KRLA2   Shrub   
 8 PSARM   Shrub   
 9 MACA17  Shrub   
10 AMDU2   Shrub   
Code
cat("\nUnique lifeforms in data:\n")

Unique lifeforms in data:
Code
print(unique(all_species_all_years$Lifeform))
[1] "Shrub" "Grass"
Code
cat("\nSpecies with Lifeform = 'Grass':\n")

Species with Lifeform = 'Grass':
Code
grass_species <- all_species_all_years %>% 
  filter(Lifeform == "Grass") %>% 
  select(species, Lifeform) %>% 
  distinct()
print(grass_species)
# A tibble: 1 × 2
  species Lifeform
  <chr>   <chr>   
1 ACHY    Grass   
Code
cat("\nSpecies with grass-related lifeforms (case insensitive):\n")

Species with grass-related lifeforms (case insensitive):
Code
grass_related <- all_species_all_years %>% 
  filter(str_detect(Lifeform, regex("grass", ignore_case = TRUE))) %>% 
  select(species, Lifeform) %>% 
  distinct()
print(grass_related)
# A tibble: 1 × 2
  species Lifeform
  <chr>   <chr>   
1 ACHY    Grass   
Code
cat("\nAll species detected in 2025 for LAW090:\n")

All species detected in 2025 for LAW090:
Code
law090_2025 <- all_species_all_years %>% 
  filter(parcel == "LAW090", year == 2025) %>% 
  select(species, Lifeform) %>% 
  distinct()
print(law090_2025)
# A tibble: 6 × 2
  species Lifeform
  <chr>   <chr>   
1 SAVE4   Shrub   
2 ATPO    Shrub   
3 ATTO    Shrub   
4 ATCA2   Shrub   
5 ERNA10  Shrub   
6 KRLA2   Shrub   
Code
# Calculate grass species presence by parcel and year
grass_analysis <- all_species_all_years %>%
  group_by(parcel, year) %>%
  summarise(
    total_species = n_distinct(species),
    grass_species_count = sum(str_detect(Lifeform, regex("grass", ignore_case = TRUE)), na.rm = TRUE),
    grass_species_list = if(grass_species_count > 0) {
      paste(unique(species[str_detect(Lifeform, regex("grass", ignore_case = TRUE))]), collapse = ", ")
    } else {
      "None"
    },
    .groups = 'drop'
  ) %>%
  mutate(
    grass_target_met = grass_species_count > 0,
    status = ifelse(grass_target_met, "✅ Grass present", "❌ No grass species")
  ) %>%
  # Shorten column names
  rename(
    "Total" = total_species,
    "Count" = grass_species_count,
    "List" = grass_species_list,
    "Target" = grass_target_met
  )

# Display grass species analysis table
DT::datatable(
  grass_analysis,
  caption = 'Grass Species Presence Analysis',
  options = list(
    pageLength = -1,  # Show all rows
    scrollX = TRUE,
    dom = 'Bfrtip',
    buttons = c('copy', 'csv', 'excel', 'pdf', 'print'),
    columnDefs = list(
      list(className = 'dt-center', targets = c(1, 2, 3, 4, 5, 6)),
      list(width = '120px', targets = 0),  # Parcel column
      list(width = '80px', targets = 1),   # Year column
      list(width = '120px', targets = 2),  # Total species
      list(width = '120px', targets = 3),  # Grass count
      list(width = '140px', targets = 4),  # Grass present
      list(width = '160px', targets = 5),  # Status
      list(width = '250px', targets = 6)   # Grass species list
    ),
    extensions = 'Buttons',
    autoWidth = TRUE
  ),
  rownames = FALSE,
  width = '100%'
) %>%
  DT::formatStyle(
    'status',
    backgroundColor = DT::styleEqual(
      c("✅ Grass present", "❌ No grass species"),
      c("#d4edda", "#f8d7da")
    )
  )

Grass Species Summary

Step 5: Comprehensive Grass Species Analysis - Show all grass species detected (both allowed and non-allowed) - Compare grass species presence between allowed and non-allowed lists - Provide complete grass species inventory by parcel and year

Code
# Get ALL grass species (not just allowable ones)
all_grass_species <- data_combined %>%
  select(parcel, year, species) %>%
  distinct() %>%
  # Join with species attributes to get lifeform information
  left_join(species_attributes_full, by = "species") %>%
  # Filter for grass species (case insensitive)
  filter(str_detect(Lifeform, regex("grass", ignore_case = TRUE))) %>%
  # Apply consistent LAW118/129 naming
  mutate(
    parcel = ifelse(parcel %in% c("LAW118", "LAW129"), "LAW118/129", parcel)
  ) %>%
  # Add allowable status
  mutate(
    parcel_group = case_when(
      parcel %in% c("LAW090", "LAW094", "LAW095") ~ "LAW90_94_95",
      parcel == "LAW118/129" ~ "LAW118_129",
      TRUE ~ "Other"
    )
  ) %>%
  mutate(
    is_allowable = species %in% allowable_species_combined$species,
    status = ifelse(is_allowable, "✅ Allowed", "❌ Not Allowed")
  ) %>%
  select(parcel, year, species, common_name, Lifeform, is_allowable, status)

# Create summary table of grass species by parcel and year
grass_summary <- all_grass_species %>%
  group_by(parcel, year) %>%
  summarise(
    total_grass_species = n(),
    allowed_grass_species = sum(is_allowable),
    non_allowed_grass_species = sum(!is_allowable),
    grass_species_list = paste(species, collapse = ", "),
    allowed_grass_list = if(sum(is_allowable) > 0) {
      paste(species[is_allowable], collapse = ", ")
    } else {
      "None"
    },
    non_allowed_grass_list = if(sum(!is_allowable) > 0) {
      paste(species[!is_allowable], collapse = ", ")
    } else {
      "None"
    },
    .groups = 'drop'
  ) %>%
  mutate(
    grass_target_met = allowed_grass_species > 0,
    status = ifelse(grass_target_met, "✅ Allowed grass present", "❌ No allowed grass")
  ) %>%
  # Remove redundant columns and shorten names
  select(-grass_species_list, -non_allowed_grass_list) %>%
  rename(
    "Total" = total_grass_species,
    "Allowed" = allowed_grass_species,
    "Non-Allowed" = non_allowed_grass_species,
    "Allowed List" = allowed_grass_list,
    "Target" = grass_target_met
  )

# Display comprehensive grass species summary
DT::datatable(
  grass_summary,
  caption = 'Comprehensive Grass Species Analysis - All Grass Species (Allowed and Non-Allowed)',
  options = list(
    pageLength = -1,  # Show all rows
    scrollX = TRUE,
    dom = 'Bfrtip',
    buttons = c('copy', 'csv', 'excel', 'pdf', 'print'),
    columnDefs = list(
      list(className = 'dt-center', targets = c(1, 2, 3, 4, 5, 6))  # Center align status columns
  ),
  extensions = 'Buttons',
    autoWidth = TRUE
  ),
  rownames = FALSE,
  width = '100%'
) %>%
  DT::formatStyle(
    'status',
    backgroundColor = DT::styleEqual(
      c("✅ Allowed grass present", "❌ No allowed grass"),
      c("#d4edda", "#f8d7da")
    )
  )
Code
# Also show detailed grass species table
cat("\n=== Detailed Grass Species by Parcel and Year ===\n")

=== Detailed Grass Species by Parcel and Year ===
Code
DT::datatable(
  all_grass_species,
  caption = 'Detailed Grass Species List - All Grass Species Detected',
  options = list(
    pageLength = 20,
    scrollX = TRUE,
    dom = 'Bfrtip',
    buttons = c('copy', 'csv', 'excel', 'pdf', 'print'),
    columnDefs = list(
      list(className = 'dt-center', targets = c(1, 2, 3, 4, 5, 6)),
      list(width = '100px', targets = 0),  # Parcel column
      list(width = '60px', targets = 1),  # Year column
      list(width = '80px', targets = 2),  # Species code
      list(width = '150px', targets = 3), # Common name
      list(width = '100px', targets = 4), # Lifeform
      list(width = '100px', targets = 5)  # Status
    ),
    extensions = 'Buttons',
    autoWidth = TRUE
  ),
  rownames = FALSE,
  width = '100%'
) %>%
  DT::formatStyle(
    'status',
    backgroundColor = DT::styleEqual(
      c("✅ Allowed", "❌ Not Allowed"),
      c("#d4edda", "#f8d7da")
    )
  )

Transect-Level Cover Analysis

Step 6: Assess Transect-Level Cover Requirements - Calculate allowable cover percentage for each individual transect - Check if each transect meets the ≥2% allowable cover requirement - Provide summary statistics of transect compliance - Identify which transects fail to meet the 2% allowable cover threshold

Code
# Function to calculate transect-level cover
calculate_transect_cover <- function(parcel_name, year_value, allowable_species_codes) {
  # Filter data for specific parcel and year
  parcel_data <- data_combined %>%
    filter(parcel == parcel_name, year == year_value)
  
  if (nrow(parcel_data) == 0) {
    return(data.frame(
      parcel = character(0),
      year = integer(0),
      transect = character(0),
      total_hits = numeric(0),
      cover_percentage = numeric(0),
      meets_2_percent = logical(0)
    ))
  }
  
  # Calculate cover for each transect
  transect_cover <- parcel_data %>%
    # Filter to only allowable species
    filter(species %in% allowable_species_codes) %>%
    group_by(transect) %>%
    summarise(
      total_hits = sum(hits, na.rm = TRUE),
      .groups = 'drop'
    ) %>%
    mutate(
      # Convert hits to cover percentage (hits/200 * 100)
      cover_percentage = (total_hits / 200) * 100,
      meets_2_percent = cover_percentage >= 2.0,
      parcel = parcel_name,
      year = year_value
    ) %>%
    select(parcel, year, transect, total_hits, cover_percentage, meets_2_percent)
  
  return(transect_cover)
}

# Debug: Check what data is available
cat("\n=== Transect Analysis Debug ===\n")

=== Transect Analysis Debug ===
Code
cat("Available parcels and years in data_combined:\n")
Available parcels and years in data_combined:
Code
print(data_combined %>% select(parcel, year) %>% distinct() %>% arrange(parcel, year))
# A tibble: 12 × 2
   parcel      year
   <chr>      <dbl>
 1 LAW090      2022
 2 LAW090      2024
 3 LAW090      2025
 4 LAW094      2022
 5 LAW094      2024
 6 LAW094      2025
 7 LAW095      2022
 8 LAW095      2024
 9 LAW095      2025
10 LAW118/129  2022
11 LAW118/129  2023
12 LAW118/129  2025
Code
cat("\nAllowable species count:", length(allowable_species_combined$species), "\n")

Allowable species count: 49 
Code
cat("Sample allowable species:", head(allowable_species_combined$species, 5), "\n")
Sample allowable species: ACHY ACSP12 ELEL5 AMDU2 ATCA2 
Code
# Calculate transect cover for all parcels and years
transect_results <- list()

# LAW090
cat("\nProcessing LAW090...\n")

Processing LAW090...
Code
transect_results[["LAW090_2022"]] <- calculate_transect_cover("LAW090", 2022, allowable_species_combined$species)
transect_results[["LAW090_2023"]] <- calculate_transect_cover("LAW090", 2023, allowable_species_combined$species)
transect_results[["LAW090_2024"]] <- calculate_transect_cover("LAW090", 2024, allowable_species_combined$species)
transect_results[["LAW090_2025"]] <- calculate_transect_cover("LAW090", 2025, allowable_species_combined$species)

# LAW094
cat("Processing LAW094...\n")
Processing LAW094...
Code
transect_results[["LAW094_2022"]] <- calculate_transect_cover("LAW094", 2022, allowable_species_combined$species)
transect_results[["LAW094_2023"]] <- calculate_transect_cover("LAW094", 2023, allowable_species_combined$species)
transect_results[["LAW094_2024"]] <- calculate_transect_cover("LAW094", 2024, allowable_species_combined$species)
transect_results[["LAW094_2025"]] <- calculate_transect_cover("LAW094", 2025, allowable_species_combined$species)

# LAW095
cat("Processing LAW095...\n")
Processing LAW095...
Code
transect_results[["LAW095_2022"]] <- calculate_transect_cover("LAW095", 2022, allowable_species_combined$species)
transect_results[["LAW095_2023"]] <- calculate_transect_cover("LAW095", 2023, allowable_species_combined$species)
transect_results[["LAW095_2024"]] <- calculate_transect_cover("LAW095", 2024, allowable_species_combined$species)
transect_results[["LAW095_2025"]] <- calculate_transect_cover("LAW095", 2025, allowable_species_combined$species)

# LAW118/129 (combined)
cat("Processing LAW118/129...\n")
Processing LAW118/129...
Code
transect_results[["LAW118_129_2022"]] <- calculate_transect_cover("LAW118/129", 2022, allowable_species_combined$species)
transect_results[["LAW118_129_2023"]] <- calculate_transect_cover("LAW118/129", 2023, allowable_species_combined$species)
transect_results[["LAW118_129_2025"]] <- calculate_transect_cover("LAW118/129", 2025, allowable_species_combined$species)

# Check results
cat("\nTransect results summary:\n")

Transect results summary:
Code
for(i in names(transect_results)) {
  cat(i, ":", nrow(transect_results[[i]]), "rows\n")
}
LAW090_2022 : 20 rows
LAW090_2023 : 0 rows
LAW090_2024 : 20 rows
LAW090_2025 : 30 rows
LAW094_2022 : 20 rows
LAW094_2023 : 0 rows
LAW094_2024 : 20 rows
LAW094_2025 : 20 rows
LAW095_2022 : 20 rows
LAW095_2023 : 0 rows
LAW095_2024 : 20 rows
LAW095_2025 : 20 rows
LAW118_129_2022 : 20 rows
LAW118_129_2023 : 15 rows
LAW118_129_2025 : 19 rows
Code
# Combine all results
all_transect_results <- bind_rows(transect_results, .id = "parcel_year") %>%
  select(-parcel_year)  # Remove the ID column

cat("\nCombined transect results:", nrow(all_transect_results), "rows\n")

Combined transect results: 244 rows
Code
if(nrow(all_transect_results) > 0) {
  cat("Sample of combined results:\n")
  print(head(all_transect_results, 3))
  
  # Debug: Check LAW118/129 2025 specifically
  cat("\n=== LAW118/129 2025 Debug ===\n")
  law118_129_2025 <- all_transect_results %>% 
    filter(parcel == "LAW118/129", year == 2025)
  cat("LAW118/129 2025 rows:", nrow(law118_129_2025), "\n")
  if(nrow(law118_129_2025) > 0) {
    cat("Cover percentages:\n")
    print(law118_129_2025$cover_percentage)
    cat("Below 2% count:", sum(law118_129_2025$cover_percentage < 2.0), "\n")
    cat("Below 2% transects:\n")
    below_2 <- law118_129_2025 %>% filter(cover_percentage < 2.0)
    print(below_2)
    
    # Check the exact threshold calculation
    cat("Threshold check (cover_percentage < 2.0):\n")
    print(law118_129_2025$cover_percentage < 2.0)
    cat("meets_2_percent values:\n")
    print(law118_129_2025$meets_2_percent)
  }
} else {
  cat("WARNING: No transect results found!\n")
}
Sample of combined results:
# A tibble: 3 × 6
  parcel  year transect total_hits cover_percentage meets_2_percent
  <chr>  <dbl> <chr>         <dbl>            <dbl> <lgl>          
1 LAW090  2022 61               22             11   TRUE           
2 LAW090  2022 62               31             15.5 TRUE           
3 LAW090  2022 63               27             13.5 TRUE           

=== LAW118/129 2025 Debug ===
LAW118/129 2025 rows: 19 
Cover percentages:
 [1] 17.5  9.5 20.5  5.5 10.5 24.0  9.0 18.0 31.0 47.5 29.0 35.0 41.0  3.0 28.0
[16] 40.5 10.5 11.0 36.5
Below 2% count: 0 
Below 2% transects:
# A tibble: 0 × 6
# ℹ 6 variables: parcel <chr>, year <dbl>, transect <chr>, total_hits <dbl>,
#   cover_percentage <dbl>, meets_2_percent <lgl>
Threshold check (cover_percentage < 2.0):
 [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[13] FALSE FALSE FALSE FALSE FALSE FALSE FALSE
meets_2_percent values:
 [1] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
[16] TRUE TRUE TRUE TRUE
Code
# Debug: Check if we have data for summary
cat("\n=== Summary Table Debug ===\n")

=== Summary Table Debug ===
Code
cat("All transect results structure:\n")
All transect results structure:
Code
str(all_transect_results)
tibble [244 × 6] (S3: tbl_df/tbl/data.frame)
 $ parcel          : chr [1:244] "LAW090" "LAW090" "LAW090" "LAW090" ...
 $ year            : num [1:244] 2022 2022 2022 2022 2022 ...
 $ transect        : chr [1:244] "61" "62" "63" "64" ...
 $ total_hits      : num [1:244] 22 31 27 21 43 33 33 15 27 21 ...
 $ cover_percentage: num [1:244] 11 15.5 13.5 10.5 21.5 16.5 16.5 7.5 13.5 10.5 ...
 $ meets_2_percent : logi [1:244] TRUE TRUE TRUE TRUE TRUE TRUE ...
Code
cat("\nUnique parcels in results:\n")

Unique parcels in results:
Code
print(unique(all_transect_results$parcel))
[1] "LAW090"     "LAW094"     "LAW095"     "LAW118/129"
Code
cat("\nUnique years in results:\n")

Unique years in results:
Code
print(unique(all_transect_results$year))
[1] 2022 2024 2025 2023
Code
# Create summary statistics
if(nrow(all_transect_results) > 0) {
  cat("Creating summary statistics...\n")
  transect_summary <- all_transect_results %>%
    group_by(parcel, year) %>%
    summarise(
      total_transects = n(),
      transects_meeting_2_percent = sum(meets_2_percent, na.rm = TRUE),
      transects_below_2_percent = sum(!meets_2_percent, na.rm = TRUE),
      compliance_rate = round((transects_meeting_2_percent / total_transects) * 100, 1),
      avg_cover_percentage = round(mean(cover_percentage, na.rm = TRUE), 2),
      min_cover_percentage = round(min(cover_percentage, na.rm = TRUE), 2),
      max_cover_percentage = round(max(cover_percentage, na.rm = TRUE), 2),
      .groups = 'drop'
    ) %>%
    mutate(
      status = ifelse(compliance_rate == 100, "✅ All transects ≥2% allowable", 
                     ifelse(compliance_rate >= 80, "⚠️ Most transects ≥2% allowable", "❌ Many transects <2% allowable"))
    ) %>%
    # Rename columns for readability
    rename(
      "Total Transects" = total_transects,
      "Meeting 2%" = transects_meeting_2_percent,
      "Below 2%" = transects_below_2_percent,
      "Compliance Rate (%)" = compliance_rate,
      "Avg Cover (%)" = avg_cover_percentage,
      "Min Cover (%)" = min_cover_percentage,
      "Max Cover (%)" = max_cover_percentage,
      "Status" = status
    )
  
  cat("Summary table created with", nrow(transect_summary), "rows\n")
  cat("Summary table structure:\n")
  str(transect_summary)
  cat("Sample summary data:\n")
  print(head(transect_summary, 3))
  
  # Debug: Check LAW118/129 2025 summary specifically
  cat("\n=== LAW118/129 2025 Summary Debug ===\n")
  law118_129_2025_summary <- transect_summary %>% 
    filter(parcel == "LAW118/129", year == 2025)
  if(nrow(law118_129_2025_summary) > 0) {
    print(law118_129_2025_summary)
  } else {
    cat("No LAW118/129 2025 summary found!\n")
  }
} else {
  cat("No data available, creating empty summary...\n")
  # Create empty summary when no data
  transect_summary <- data.frame(
    parcel = character(0),
    year = integer(0),
    total_transects = integer(0),
    transects_meeting_2_percent = integer(0),
    transects_below_2_percent = integer(0),
    compliance_rate = numeric(0),
    avg_cover_percentage = numeric(0),
    min_cover_percentage = numeric(0),
    max_cover_percentage = numeric(0),
    status = character(0)
  )
}
Creating summary statistics...
Summary table created with 12 rows
Summary table structure:
tibble [12 × 10] (S3: tbl_df/tbl/data.frame)
 $ parcel             : chr [1:12] "LAW090" "LAW090" "LAW090" "LAW094" ...
 $ year               : num [1:12] 2022 2024 2025 2022 2024 ...
 $ Total Transects    : int [1:12] 20 20 30 20 20 20 20 20 20 20 ...
 $ Meeting 2%         : int [1:12] 20 20 30 20 20 20 20 19 20 20 ...
 $ Below 2%           : int [1:12] 0 0 0 0 0 0 0 1 0 0 ...
 $ Compliance Rate (%): num [1:12] 100 100 100 100 100 100 100 95 100 100 ...
 $ Avg Cover (%)      : num [1:12] 15.2 13.07 12.48 10.03 9.65 ...
 $ Min Cover (%)      : num [1:12] 7 4.5 2 5.5 3.5 2.5 3 1 2 4 ...
 $ Max Cover (%)      : num [1:12] 24.5 24 29 14.5 20.5 18 23.5 29 17.5 34.5 ...
 $ Status             : chr [1:12] "✅ All transects ≥2% allowable" "✅ All transects ≥2% allowable" "✅ All transects ≥2% allowable" "✅ All transects ≥2% allowable" ...
Sample summary data:
# A tibble: 3 × 10
  parcel  year `Total Transects` `Meeting 2%` `Below 2%` `Compliance Rate (%)`
  <chr>  <dbl>             <int>        <int>      <int>                 <dbl>
1 LAW090  2022                20           20          0                   100
2 LAW090  2024                20           20          0                   100
3 LAW090  2025                30           30          0                   100
# ℹ 4 more variables: `Avg Cover (%)` <dbl>, `Min Cover (%)` <dbl>,
#   `Max Cover (%)` <dbl>, Status <chr>

=== LAW118/129 2025 Summary Debug ===
# A tibble: 1 × 10
  parcel    year `Total Transects` `Meeting 2%` `Below 2%` `Compliance Rate (%)`
  <chr>    <dbl>             <int>        <int>      <int>                 <dbl>
1 LAW118/…  2025                19           19          0                   100
# ℹ 4 more variables: `Avg Cover (%)` <dbl>, `Min Cover (%)` <dbl>,
#   `Max Cover (%)` <dbl>, Status <chr>
Code
# Display transect summary table
DT::datatable(
  transect_summary,
  caption = 'Transect-Level Cover Analysis Summary - 2% Allowable Cover Requirement',
  options = list(
    pageLength = -1,  # Show all rows
    scrollX = TRUE,
    dom = 'Bfrtip',
    buttons = c('copy', 'csv', 'excel', 'pdf', 'print'),
    columnDefs = list(
      list(className = 'dt-center', targets = c(1, 2, 3, 4, 5, 6, 7, 8, 9)),
      list(width = '120px', targets = 0),  # Parcel column
      list(width = '80px', targets = 1),   # Year column
      list(width = '120px', targets = 2),  # Total transects
      list(width = '140px', targets = 3), # Meeting 2%
      list(width = '120px', targets = 4), # Below 2%
      list(width = '160px', targets = 5), # Compliance rate
      list(width = '140px', targets = 6), # Avg cover
      list(width = '140px', targets = 7), # Min cover
      list(width = '140px', targets = 8), # Max cover
      list(width = '200px', targets = 9)  # Status
    ),
    extensions = 'Buttons',
    autoWidth = TRUE
  ),
  rownames = FALSE,
  width = '100%'
) %>%
  DT::formatStyle(
    'Status',
    backgroundColor = DT::styleEqual(
      c("✅ All transects ≥2% allowable", "⚠️ Most transects ≥2% allowable", "❌ Many transects <2% allowable"),
      c("#d4edda", "#fff3cd", "#f8d7da")
    )
  )
Code
# Display detailed transect results
if(nrow(all_transect_results) > 0) {
  DT::datatable(
    all_transect_results,
    caption = 'Detailed Transect-Level Cover Analysis - Individual Transect Results (Allowable Species Only)',
  options = list(
    pageLength = 20,
    scrollX = TRUE,
    dom = 'Bfrtip',
    buttons = c('copy', 'csv', 'excel', 'pdf', 'print'),
    columnDefs = list(
      list(className = 'dt-center', targets = c(1, 2, 3, 4, 5)),
      list(width = '100px', targets = 0),  # Parcel column
      list(width = '60px', targets = 1),  # Year column
      list(width = '80px', targets = 2),  # Transect column
      list(width = '80px', targets = 3),  # Total hits
      list(width = '100px', targets = 4), # Cover percentage
      list(width = '100px', targets = 5)  # Meets 2%
    ),
    extensions = 'Buttons',
    autoWidth = TRUE
  ),
  rownames = FALSE,
  width = '100%'
) %>%
  DT::formatStyle(
    'meets_2_percent',
    backgroundColor = DT::styleEqual(
      c(TRUE, FALSE),
      c("#d4edda", "#f8d7da")
    )
  ) %>%
  DT::formatRound(
    columns = 'cover_percentage',
    digits = 2
  )
} else {
  cat("\n=== No Transect Data Available ===\n")
  cat("No transect-level data found. This could be due to:\n")
  cat("1. No data available for the specified parcels/years\n")
  cat("2. No allowable species found in the data\n")
  cat("3. Data filtering issues\n")
  cat("Please check the debug output above for more details.\n")
}

Species with 3+ Hits Analysis

Step 7: Assess Species with 3+ Hits Requirement - Count species with 3+ hits for each parcel and year - Check if each parcel meets the ≥6 species with 3+ hits requirement - Filter to only allowable species (including ATTO, ERNA10, ATPO) - Provide summary statistics of species diversity compliance

Code
# Function to calculate species with 3+ hits for each parcel and year
calculate_species_3plus_hits <- function(parcel_name, year_value, allowable_species_codes) {
  # Filter data for specific parcel and year
  parcel_data <- data_combined %>%
    filter(parcel == parcel_name, year == year_value)
  
  if (nrow(parcel_data) == 0) {
    return(data.frame(
      parcel = parcel_name,
      year = year_value,
      species_3plus_count = 0,
      meets_6_species = FALSE,
      species_3plus_list = "None"
    ))
  }
  
  # Calculate species with 3+ hits (allowable species only)
  species_3plus <- parcel_data %>%
    # Filter to only allowable species
    filter(species %in% allowable_species_codes) %>%
    group_by(species) %>%
    summarise(
      total_hits = sum(hits, na.rm = TRUE),
      .groups = 'drop'
    ) %>%
    filter(total_hits >= 3) %>%
    arrange(desc(total_hits))
  
  # Create summary
  species_3plus_count <- nrow(species_3plus)
  meets_6_species <- species_3plus_count >= 6
  species_3plus_list <- if(species_3plus_count > 0) {
    paste(species_3plus$species, collapse = ", ")
  } else {
    "None"
  }
  
  return(data.frame(
    parcel = parcel_name,
    year = year_value,
    species_3plus_count = species_3plus_count,
    meets_6_species = meets_6_species,
    species_3plus_list = species_3plus_list
  ))
}

# Calculate species with 3+ hits for all parcels and years
species_3plus_results <- list()

# LAW090
cat("Processing LAW090 species with 3+ hits...\n")
Processing LAW090 species with 3+ hits...
Code
species_3plus_results[["LAW090_2022"]] <- calculate_species_3plus_hits("LAW090", 2022, allowable_species_combined$species)
species_3plus_results[["LAW090_2023"]] <- calculate_species_3plus_hits("LAW090", 2023, allowable_species_combined$species)
species_3plus_results[["LAW090_2024"]] <- calculate_species_3plus_hits("LAW090", 2024, allowable_species_combined$species)
species_3plus_results[["LAW090_2025"]] <- calculate_species_3plus_hits("LAW090", 2025, allowable_species_combined$species)

# LAW094
cat("Processing LAW094 species with 3+ hits...\n")
Processing LAW094 species with 3+ hits...
Code
species_3plus_results[["LAW094_2022"]] <- calculate_species_3plus_hits("LAW094", 2022, allowable_species_combined$species)
species_3plus_results[["LAW094_2023"]] <- calculate_species_3plus_hits("LAW094", 2023, allowable_species_combined$species)
species_3plus_results[["LAW094_2024"]] <- calculate_species_3plus_hits("LAW094", 2024, allowable_species_combined$species)
species_3plus_results[["LAW094_2025"]] <- calculate_species_3plus_hits("LAW094", 2025, allowable_species_combined$species)

# LAW095
cat("Processing LAW095 species with 3+ hits...\n")
Processing LAW095 species with 3+ hits...
Code
species_3plus_results[["LAW095_2022"]] <- calculate_species_3plus_hits("LAW095", 2022, allowable_species_combined$species)
species_3plus_results[["LAW095_2023"]] <- calculate_species_3plus_hits("LAW095", 2023, allowable_species_combined$species)
species_3plus_results[["LAW095_2024"]] <- calculate_species_3plus_hits("LAW095", 2024, allowable_species_combined$species)
species_3plus_results[["LAW095_2025"]] <- calculate_species_3plus_hits("LAW095", 2025, allowable_species_combined$species)

# LAW118/129 (combined)
cat("Processing LAW118/129 species with 3+ hits...\n")
Processing LAW118/129 species with 3+ hits...
Code
species_3plus_results[["LAW118_129_2022"]] <- calculate_species_3plus_hits("LAW118/129", 2022, allowable_species_combined$species)
species_3plus_results[["LAW118_129_2023"]] <- calculate_species_3plus_hits("LAW118/129", 2023, allowable_species_combined$species)
species_3plus_results[["LAW118_129_2025"]] <- calculate_species_3plus_hits("LAW118/129", 2025, allowable_species_combined$species)

# Combine all results
all_species_3plus_results <- bind_rows(species_3plus_results, .id = "parcel_year") %>%
  select(-parcel_year)  # Remove the ID column

cat("\nSpecies with 3+ hits results:", nrow(all_species_3plus_results), "rows\n")

Species with 3+ hits results: 15 rows
Code
if(nrow(all_species_3plus_results) > 0) {
  cat("Sample results:\n")
  print(head(all_species_3plus_results, 3))
}
Sample results:
  parcel year species_3plus_count meets_6_species
1 LAW090 2022                   6            TRUE
2 LAW090 2023                   0           FALSE
3 LAW090 2024                   7            TRUE
                              species_3plus_list
1        ATPO, ATCA2, ATTO, ERNA10, KRLA2, SAVE4
2                                           None
3 ATPO, ATCA2, ATTO, ERNA10, AMDU2, KRLA2, SAVE4
Code
# Create summary statistics (simplified) - only show monitored years
# Define which years were actually monitored for each parcel
monitored_years <- list(
  "LAW090" = c(2022, 2024, 2025),
  "LAW094" = c(2022, 2024, 2025), 
  "LAW095" = c(2022, 2024, 2025),
  "LAW118/129" = c(2022, 2023, 2025)
)

species_3plus_summary <- all_species_3plus_results %>%
  # Filter to only show parcel-years that were actually monitored
  filter(
    (parcel == "LAW090" & year %in% monitored_years[["LAW090"]]) |
    (parcel == "LAW094" & year %in% monitored_years[["LAW094"]]) |
    (parcel == "LAW095" & year %in% monitored_years[["LAW095"]]) |
    (parcel == "LAW118/129" & year %in% monitored_years[["LAW118/129"]])
  ) %>%
  select(parcel, year, species_3plus_count, meets_6_species, species_3plus_list) %>%
  mutate(
    status = ifelse(meets_6_species, "✅", "❌")
  ) %>%
  # Rename columns for readability
  rename(
    "Count" = species_3plus_count,
    "Target" = meets_6_species,
    "Species" = species_3plus_list,
    "Status" = status
  )

# Display species with 3+ hits summary table
DT::datatable(
  species_3plus_summary,
  caption = 'Species with 3+ Hits Analysis Summary - 6 Species Requirement',
  options = list(
    pageLength = -1,  # Show all rows
    scrollX = TRUE,
    dom = 'Bfrtip',
    buttons = c('copy', 'csv', 'excel', 'pdf', 'print'),
    columnDefs = list(
      list(className = 'dt-center', targets = c(1, 2, 3, 4, 5)),
      list(width = '80px', targets = 0),  # Parcel column
      list(width = '50px', targets = 1),  # Year column
      list(width = '60px', targets = 2), # Count
      list(width = '60px', targets = 3), # Target
      list(width = '150px', targets = 4), # Species list
      list(width = '60px', targets = 5)   # Status
    ),
  extensions = 'Buttons',
    autoWidth = TRUE
  ),
  rownames = FALSE,
  width = '100%'
) %>%
  DT::formatStyle(
    'Status',
    backgroundColor = DT::styleEqual(
      c("✅", "❌"),
      c("#d4edda", "#f8d7da")
    )
  ) %>%
  DT::formatStyle(
    'Target',
    backgroundColor = DT::styleEqual(
      c(TRUE, FALSE),
      c("#d4edda", "#f8d7da")
    )
  )
Code
# Note: Detailed table removed - all information now in summary table above

Comprehensive Target Status Summary

Step 8: Comprehensive Target Assessment - Combine all revegetation target statuses into a single summary table - Show overall target achievement across all metrics for each parcel-year - Provide complete compliance overview for monitoring and reporting

Column Descriptions

This table provides a comprehensive overview of revegetation target achievement across all Laws parcels and monitoring years. Each column represents a specific target metric:

  • Cover: Overall allowable species cover target (≥10% required)
  • Grass: Presence of at least one grass species (required for all parcels)
  • Transect: Every transect >2% cover
  • Species 3+: ≥6 species with 3+ hits
  • Overall: Combined status across all target metrics

Status Indicators: - ✅ Green: Target met/compliant - ❌ Red: Target not met/non-compliant

Code
# Debug: Check what columns exist in dashboard_data
cat("Columns in dashboard_data:\n")
Columns in dashboard_data:
Code
print(names(dashboard_data))
[1] "Parcel"                   "Year"                    
[3] "Cover.."                  "Transects"               
[5] "Cover.After.Species.Caps" "Allowed.Sp.Count"        
[7] "Cover.Status"            
Code
cat("Sample of dashboard_data:\n")
Sample of dashboard_data:
Code
print(head(dashboard_data, 2))
  Parcel Year Cover.. Transects Cover.After.Species.Caps Allowed.Sp.Count
1 LAW090 2022   15.20        20                    11.43                8
2 LAW090 2024   13.07        20                    10.00                7
  Cover.Status
1 ✅ Compliant
2 ✅ Compliant
Code
# Check if dashboard_data exists and has the right structure
if (!exists("dashboard_data")) {
  cat("ERROR: dashboard_data does not exist!\n")
} else {
  cat("dashboard_data exists with", nrow(dashboard_data), "rows and", ncol(dashboard_data), "columns\n")
  cat("Column names:", paste(names(dashboard_data), collapse = ", "), "\n")
}
dashboard_data exists with 12 rows and 7 columns
Column names: Parcel, Year, Cover.., Transects, Cover.After.Species.Caps, Allowed.Sp.Count, Cover.Status 
Code
# Get the analytical results (cover targets)
if (exists("dashboard_data") && ncol(dashboard_data) >= 7) {
  cover_results <- dashboard_data %>%
    select(1, 2, 7) %>%  # Parcel, Year, Cover Status (7th column)
    rename(parcel = 1, year = 2, cover_status = 3)
  
  # Debug: Check cover_results
  cat("Cover results dimensions:", dim(cover_results), "\n")
  print(head(cover_results, 2))
} else {
  cat("ERROR: dashboard_data doesn't have enough columns or doesn't exist\n")
  cover_results <- data.frame()
}
Cover results dimensions: 12 3 
  parcel year cover_status
1 LAW090 2022 ✅ Compliant
2 LAW090 2024 ✅ Compliant
Code
# Get grass species results
if (exists("grass_analysis") && "Target" %in% names(grass_analysis)) {
  grass_results <- grass_analysis %>%
    select(parcel, year, Target) %>%
    mutate(grass_status = ifelse(Target, "✅", "❌")) %>%
    select(parcel, year, grass_status)  # Remove Target column
  
  # Debug: Check grass_results
  cat("Grass results dimensions:", dim(grass_results), "\n")
  print(head(grass_results, 2))
} else {
  cat("ERROR: grass_analysis doesn't exist or doesn't have Target column\n")
  grass_results <- data.frame()
}
Grass results dimensions: 12 3 
# A tibble: 2 × 3
  parcel  year grass_status
  <chr>  <dbl> <chr>       
1 LAW090  2022 ✅          
2 LAW090  2024 ❌          
Code
# Get transect cover results (2% allowable cover)
if (exists("transect_summary") && "Compliance Rate (%)" %in% names(transect_summary)) {
  transect_results <- transect_summary %>%
    select(parcel, year, `Compliance Rate (%)`) %>%
    mutate(
      transect_status = case_when(
        `Compliance Rate (%)` == 100 ~ "✅",
        TRUE ~ "❌"
      )
    ) %>%
    select(parcel, year, transect_status)
} else {
  cat("ERROR: transect_summary doesn't exist or doesn't have Compliance Rate (%) column\n")
  transect_results <- data.frame()
}

# Get species with 3+ hits results
if (exists("all_species_3plus_results") && "meets_6_species" %in% names(all_species_3plus_results)) {
  species_3plus_results_summary <- all_species_3plus_results %>%
    select(parcel, year, meets_6_species) %>%
    mutate(species_3plus_status = ifelse(meets_6_species, "✅", "❌"))
} else {
  cat("ERROR: all_species_3plus_results doesn't exist or doesn't have meets_6_species column\n")
  species_3plus_results_summary <- data.frame()
}

# Combine all results
if (nrow(cover_results) > 0) {
  comprehensive_summary <- cover_results %>%
    left_join(grass_results, by = c("parcel", "year")) %>%
    left_join(transect_results, by = c("parcel", "year")) %>%
    left_join(species_3plus_results_summary, by = c("parcel", "year")) %>%
    # Create overall status (two-tiered: All Targets Met or Targets Not Met)
    mutate(
      overall_status = case_when(
        cover_status == "✅ Compliant" & 
        grass_status == "✅" & 
        transect_status == "✅" & 
        species_3plus_status == "✅" ~ "✅ All Targets Met",
        TRUE ~ "❌ Targets Not Met"
      )
    ) %>%
  # Simplify cover status to emoji only
  mutate(
    cover_status = case_when(
      cover_status == "✅ Compliant" ~ "✅",
      cover_status == "❌ Non-compliant" ~ "❌",
      TRUE ~ cover_status
    )
  ) %>%
  # Remove redundant boolean columns and rename for display
  select(-meets_6_species) %>%
  rename(
    "Cover" = cover_status,
    "Grass" = grass_status,
    "Transect" = transect_status,
    "Species 3+" = species_3plus_status,
    "Overall" = overall_status
  )
  
  # Debug: Check the comprehensive_summary data
  cat("Comprehensive summary dimensions:", dim(comprehensive_summary), "\n")
  cat("Comprehensive summary columns:", names(comprehensive_summary), "\n")
  print(head(comprehensive_summary, 3))
  
  # Display comprehensive target summary - try different approaches
  cat("Creating interactive table...\n")
  
  # Check if DT is available
  if (requireNamespace("DT", quietly = TRUE)) {
    cat("DT library is available\n")
    
    # Try DT table with proper column widths
    tryCatch({
      simple_table <- DT::datatable(
        comprehensive_summary,
        caption = 'Comprehensive Revegetation Target Status Summary - All Parcels and Years (Cover: ≥10% allowable species, Grass: presence required, Every Transect: >2% cover, ≥6 species with 3+ hits, Overall: combined status)',
        options = list(
          pageLength = -1,
          autoWidth = TRUE,
          columnDefs = list(
            list(className = 'dt-center', targets = c(1, 2, 3, 4, 5, 6))  # Center align status columns
          )
        ),
        rownames = FALSE
      ) %>%
      DT::formatStyle(
        'Cover',
        backgroundColor = DT::styleEqual(
          c("✅", "❌"),
          c("#d4edda", "#f8d7da")
        )
      ) %>%
      DT::formatStyle(
        'Grass',
        backgroundColor = DT::styleEqual(
          c("✅", "❌"),
          c("#d4edda", "#f8d7da")
        )
      ) %>%
      DT::formatStyle(
        'Transect',
        backgroundColor = DT::styleEqual(
          c("✅", "⚠️", "❌"),
          c("#d4edda", "#fff3cd", "#f8d7da")
        )
      ) %>%
      DT::formatStyle(
        'Species 3+',
        backgroundColor = DT::styleEqual(
          c("✅", "❌"),
          c("#d4edda", "#f8d7da")
        )
      ) %>%
      DT::formatStyle(
        'Overall',
        backgroundColor = DT::styleEqual(
          c("✅ All Targets Met", "❌ Targets Not Met"),
          c("#d4edda", "#f8d7da")
        )
      )
      cat("DT table created successfully\n")
      simple_table
    }, error = function(e) {
      cat("DT table creation failed:", e$message, "\n")
      cat("Falling back to basic table display\n")
      knitr::kable(comprehensive_summary, caption = "Comprehensive Revegetation Target Status Summary")
    })
  } else {
    cat("DT library not available, using basic table\n")
    knitr::kable(comprehensive_summary, caption = "Comprehensive Revegetation Target Status Summary")
  }
} else {
  cat("ERROR: Cannot create comprehensive summary - no data available\n")
  cat("Cover results rows:", nrow(cover_results), "\n")
  cat("Grass results rows:", nrow(grass_results), "\n")
  cat("Transect results rows:", nrow(transect_results), "\n")
  cat("Species 3+ results rows:", nrow(species_3plus_results_summary), "\n")
}
Comprehensive summary dimensions: 12 7 
Comprehensive summary columns: parcel year Cover Grass Transect Species 3+ Overall 
  parcel year Cover Grass Transect Species 3+            Overall
1 LAW090 2022    ✅    ✅       ✅         ✅ ✅ All Targets Met
2 LAW090 2024    ✅    ❌       ✅         ✅ ❌ Targets Not Met
3 LAW090 2025    ✅    ❌       ✅         ✅ ❌ Targets Not Met
Creating interactive table...
DT library is available
DT table created successfully

Data Sources

Overview

This analysis integrates multiple data sources to provide comprehensive revegetation target assessment across all Laws parcels. The following datasets are available for download:

Data Processing Summary

Raw Data Sources - LAW090/094/095: LADWP Excel files (2022-2025) - LAW118/129: ICWD CSV files (2022-2025) - Species Lists: Allowable species by parcel group - Reference Data: Policy thresholds and capping rules

Processing Steps 1. Data Loading: Import and clean raw survey data 2. Species Filtering: Apply allowable species lists 3. Cover Calculation: Convert hits to percentage cover 4. Capping Rules: Apply species-specific limits 5. Compliance Assessment: Evaluate against 5 target criteria 6. Export Results: Generate summary tables and spatial data

Available Datasets

📥 Input Data

Primary Monitoring Data

LAW090/094/095 Parcels (2022-2025) - Source: Los Angeles Department of Water and Power (LADWP) - Files: - 2022 Data - LAW090/094/095 monitoring data - 2024 Data - LAW090/094/095 monitoring data - 2025 Data - LAW090/094/095 Excel data - Content: Point-intercept survey data with species hits per transect - Cover Conversion: Raw hits converted to percentage cover (hits/200 × 100) - Years: 2022, 2024, 2025 (no monitoring in 2023)

LAW118/129 Parcels (2022-2025) - Source: Inyo County Water Department (ICWD) - Files: - 2022 Data - LAW118/129 monitoring data - 2023 Data - LAW118/129 monitoring data - 2025 Data - LAW118/129 monitoring data - Content: Point-intercept survey data for combined LAW118/129 parcel - Cover Conversion: Raw hits converted to percentage cover (hits/200 × 100) - Years: 2022, 2023, 2025 (no monitoring in 2024)

🔧 Auxiliary Data

Species Reference Data

  • File: TypeE_Transfer_SppList.xlsx
  • Content: Species lists defining allowable species for revegetation targets
  • Groups:
    • LAW90_94_95: Allowable species for LAW090, LAW094, LAW095
    • LAW118_129: Allowable species for LAW118/129 combined parcel
  • Core Species: ERNA10, ATTO, ATPO automatically included in all lists

Species Attributes

  • File: species.csv - Species codes, names, and classifications
  • Content: Complete species database with lifeform, lifecycle, and common names
  • Usage: Species identification and classification for analysis

📊 Processed Data

The analysis generates several key datasets that are available for download:

Merged Revegetation Data

  • Analytical Results Table - Complete merged dataset with cover conversion and species attributes
  • Content: Raw survey data with hits converted to cover percentages, species attributes joined, and capping rules applied
  • Usage: Primary dataset for all downstream analysis and compliance calculations

Spatial Data

📋 Summary Tables

Parcel-Level Summaries

  • Parcel Summary - Parcel-level compliance statistics by year
  • Species Summary - Species diversity and compliance across all years
  • Transect Summary - Transect-level cover analysis and compliance
  • Content: Aggregated statistics by parcel and year
  • Usage: Parcel-level performance assessment

Compliance Assessment

  • Comprehensive Compliance Summary - Main compliance results with all target metrics
  • Content: Parcel-year combinations with compliance status for all 5 targets
  • Usage: Primary compliance assessment results

Reference Parcel Analysis

  • Reference Parcel Summary - Reference parcel statistics and ATTO/ERNA analysis
  • ATTO/ERNA Analysis - Detailed analysis of policy-capped species
  • Content: Reference parcel data and species analysis
  • Usage: Reference data for comparison with revegetation parcels

📦 Data Download

Export Results

Comprehensive compliance summary exported to CSV
Transect summary exported to CSV
Species summary for all years exported to CSV
Parcel summary exported to CSV
[1] TRUE
[1] TRUE
✅ Clean downloads package created
📁 Clean directory: output/clean_downloads 
📊 Files included: 10 
📦 ZIP file: output/clean_revegetation_data.zip 
[1] TRUE
[1] TRUE
[1] TRUE
[1] TRUE
[1] TRUE
[1] TRUE
[1] TRUE
[1] TRUE
[1] TRUE
[1] TRUE
[1] TRUE
[1] TRUE
Results exported to output/ and docs/ directories
Primary and auxiliary data files copied to docs/ directory

Citation

BibTeX citation:
@report{2025,
  author = {},
  publisher = {Inyo County Water Department},
  title = {Laws {Revegetation} {Data} {Analysis}},
  date = {2025-09-03},
  url = {https://github.com/inyo-gov/revegetation-projects},
  langid = {en}
}
For attribution, please cite this work as:
“Laws Revegetation Data Analysis.” 2025. Data Report. Inyo County Water Department. https://github.com/inyo-gov/revegetation-projects.