Skip to content

Commit 311362d

Browse files
authored
feat: Creates the GBI report page (#10)
This pull request significantly enhances the application's data reporting capabilities by introducing a new, comprehensive GBI (Grassland Butterfly Index) report page. This page provides a detailed overview of butterfly population trends, including regional breakdowns and in-depth visualizations of individual species trends with improved statistical confidence intervals. The changes aim to offer users a more robust and insightful analysis of grassland butterfly populations. ## Highlights - New GBI Report Page: A dedicated page has been created to display a comprehensive Grassland Butterfly Index (GBI) report, accessible via the /gbi route. - Regional GBI Calculation and Display: The data processing now includes the calculation and storage of regional GBI data for different climatic regions, which is then visualized on the new report page. - Enhanced Trend Confidence Intervals: Trend calculation logic has been updated to include 80% confidence intervals alongside the existing 95% intervals, providing more detailed statistical insights for both overall and species-specific trends. - Detailed Species Trend Visualizations: The GBI report page now features individual trend charts for each grassland butterfly species, along with a bar chart visualizing their annual rate of change, complete with 80% and 95% confidence intervals. - UI/UX Improvements and Data Consistency: The main transects page now includes a GBI trend statistic linking to the new report, and trend classification badges across the application utilize centralized helper functions for consistent styling and localization.
1 parent d08c743 commit 311362d

18 files changed

+3066
-814
lines changed

public/data/flight-curves-data.json

Lines changed: 586 additions & 66 deletions
Large diffs are not rendered by default.

public/data/processed-transects.json

Lines changed: 205 additions & 205 deletions
Large diffs are not rendered by default.

public/data/regional-gbi-data.json

Lines changed: 410 additions & 0 deletions
Large diffs are not rendered by default.

scripts/process-butterfly-data.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
processTimelineData,
3939
processMunicipalityGeoJSON,
4040
} from "./processors";
41+
import { calculateRegionalGBI } from "./processors/regional-gbi-processor";
4142

4243
/**
4344
* Main processing function
@@ -378,6 +379,21 @@ async function processData(): Promise<void> {
378379
console.warn("\nWarning: Flight curves calculation failed or returned no data");
379380
}
380381

382+
// Calculate and save regional GBI data
383+
const regionalGBIData = await calculateRegionalGBI(allData, results, BASELINE_YEAR);
384+
385+
if (regionalGBIData && Object.keys(regionalGBIData.regions).length > 0) {
386+
const REGIONAL_GBI_OUTPUT_FILE = path.join(OUTPUT_DIR, "regional-gbi-data.json");
387+
console.log(`\nWriting regional GBI data to ${REGIONAL_GBI_OUTPUT_FILE}...`);
388+
fs.writeFileSync(REGIONAL_GBI_OUTPUT_FILE, JSON.stringify(regionalGBIData, null, 2), "utf-8");
389+
console.log(
390+
`Regional GBI data saved (${(fs.statSync(REGIONAL_GBI_OUTPUT_FILE).size / 1024).toFixed(2)} KB)`
391+
);
392+
console.log(` - Regions with GBI: ${Object.keys(regionalGBIData.regions).length}`);
393+
} else {
394+
console.warn("\nWarning: Regional GBI calculation failed or returned no data");
395+
}
396+
381397
// Process municipality species map
382398
processMunicipalityGeoJSON(results, allData);
383399

scripts/processors/flight-curves-processor.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,10 +258,18 @@ export async function calculateAllFlightCurves(
258258
lower: trendStats.pc1_ci_lower || null,
259259
upper: trendStats.pc1_ci_upper || null,
260260
},
261+
confidenceInterval80: {
262+
lower: trendStats.pc1_ci_80_lower || null,
263+
upper: trendStats.pc1_ci_80_upper || null,
264+
},
261265
rateCI: {
262266
lower: trendStats.rate_ci_lower || null,
263267
upper: trendStats.rate_ci_upper || null,
264268
},
269+
rateCI80: {
270+
lower: trendStats.rate_ci_80_lower || null,
271+
upper: trendStats.rate_ci_80_upper || null,
272+
},
265273
};
266274
}
267275

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* Regional Grassland Butterfly Index (GBI) processor
3+
* Calculates GBI separately for each climatic region with sufficient data
4+
*/
5+
6+
import { calculateGBI } from "./gbi-processor";
7+
import { getQualityFilteredTransects } from "./transect-stats-processor";
8+
import { TransectStats } from "../../src/types/processing";
9+
import type { RegionalGBICollection, RegionalGBIData } from "../../src/types/gbiData";
10+
import {
11+
MIN_TRANSECTS_FOR_REGIONAL_GBI,
12+
MIN_YEARS_FOR_REGIONAL_GBI,
13+
BASELINE_YEAR,
14+
} from "../../src/constants";
15+
16+
/**
17+
* Calculate regional GBI for each climatic region
18+
*/
19+
export async function calculateRegionalGBI(
20+
allData: Record<string, string>[],
21+
transects: TransectStats[],
22+
baselineYear: number = BASELINE_YEAR
23+
): Promise<RegionalGBICollection> {
24+
console.log("\nCalculating Regional Grassland Butterfly Index (GBI)...");
25+
26+
// Step 1: Get quality transects and filter to active ones
27+
const qualityTransects = getQualityFilteredTransects(transects);
28+
const activeQualityTransects = qualityTransects.filter(t => t.isActive);
29+
30+
console.log(` Total active quality transects: ${activeQualityTransects.length}`);
31+
32+
// Step 2: Group transects by climatic region
33+
const transectsByRegion: Record<string, TransectStats[]> = {};
34+
35+
for (const transect of activeQualityTransects) {
36+
const region = transect.climaticRegion || "Unknown";
37+
if (!transectsByRegion[region]) {
38+
transectsByRegion[region] = [];
39+
}
40+
transectsByRegion[region].push(transect);
41+
}
42+
43+
const regionNames = Object.keys(transectsByRegion).sort();
44+
console.log(` Regions found: ${regionNames.length}`);
45+
46+
regionNames.forEach(region => {
47+
console.log(
48+
` - ${region}: ${transectsByRegion[region].length} transect${transectsByRegion[region].length !== 1 ? "s" : ""}`
49+
);
50+
});
51+
52+
// Step 3: Calculate GBI for each region with sufficient data
53+
const regions: Record<string, RegionalGBIData> = {};
54+
55+
for (const region of regionNames) {
56+
const regionTransects = transectsByRegion[region];
57+
const transectCount = regionTransects.length;
58+
59+
console.log(`\n Processing region: ${region}`);
60+
console.log(` Transects: ${transectCount}`);
61+
62+
// Check if region has sufficient transects
63+
if (transectCount < MIN_TRANSECTS_FOR_REGIONAL_GBI) {
64+
console.log(
65+
` Skipping: insufficient transects (minimum ${MIN_TRANSECTS_FOR_REGIONAL_GBI} required)`
66+
);
67+
continue;
68+
}
69+
70+
// Filter allData to only include observations from this region's transects
71+
const regionTransectIds = new Set(regionTransects.map(t => t.transectId));
72+
const regionalData = allData.filter(row => regionTransectIds.has(row["Transect ID"]));
73+
74+
console.log(` Filtered data: ${regionalData.length} observations`);
75+
76+
// Calculate GBI for this region
77+
const regionalGBI = await calculateGBI(regionalData, regionTransects, baselineYear);
78+
79+
if (!regionalGBI) {
80+
console.log(` Skipping: GBI calculation failed`);
81+
continue;
82+
}
83+
84+
// Check if we have sufficient years
85+
const yearsCount = regionalGBI.years.length;
86+
if (yearsCount < MIN_YEARS_FOR_REGIONAL_GBI) {
87+
console.log(
88+
` Skipping: insufficient years (${yearsCount}, minimum ${MIN_YEARS_FOR_REGIONAL_GBI} required)`
89+
);
90+
continue;
91+
}
92+
93+
// Count species included in this region's GBI
94+
const speciesIncluded = regionalGBI.metadata.grasslandSpecies.length;
95+
96+
console.log(` ✓ Regional GBI calculated`);
97+
console.log(` - Years: ${yearsCount}`);
98+
console.log(` - Species: ${speciesIncluded}`);
99+
console.log(
100+
` - Trend: ${regionalGBI.gbiTrend?.category || "N/A"} (${(regionalGBI.gbiTrend?.pc1 || 0).toFixed(1)}%/yr)`
101+
);
102+
103+
// Store regional data
104+
regions[region] = {
105+
region,
106+
transectCount,
107+
transectIds: regionTransects.map(t => t.transectId),
108+
gbiByYear: regionalGBI.gbiByYear,
109+
years: regionalGBI.years,
110+
gbiTrend: regionalGBI.gbiTrend || null,
111+
dataQuality: {
112+
sufficientData: true,
113+
minTransectsRequired: MIN_TRANSECTS_FOR_REGIONAL_GBI,
114+
minYearsRequired: MIN_YEARS_FOR_REGIONAL_GBI,
115+
speciesIncluded,
116+
},
117+
};
118+
}
119+
120+
const regionsWithGBI = Object.keys(regions).length;
121+
console.log(`\n ✓ Regional GBI calculation complete`);
122+
console.log(` Regions processed: ${regionsWithGBI}/${regionNames.length}`);
123+
124+
// Step 4: Return collection
125+
return {
126+
regions,
127+
metadata: {
128+
calculatedAt: new Date().toISOString(),
129+
baselineYear,
130+
minimumTransects: MIN_TRANSECTS_FOR_REGIONAL_GBI,
131+
minimumYears: MIN_YEARS_FOR_REGIONAL_GBI,
132+
},
133+
};
134+
}

scripts/rbms-collated-index.R

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -696,9 +696,13 @@ calculate_trend_with_ci <- function(collind_boot, baseline_year) {
696696
rate = NA,
697697
rate_ci_lower = NA,
698698
rate_ci_upper = NA,
699+
rate_ci_80_lower = NA,
700+
rate_ci_80_upper = NA,
699701
pc1 = NA,
700702
pc1_ci_lower = NA,
701703
pc1_ci_upper = NA,
704+
pc1_ci_80_lower = NA,
705+
pc1_ci_80_upper = NA,
702706
trend_class = "Uncertain"
703707
))
704708
}
@@ -725,19 +729,28 @@ calculate_trend_with_ci <- function(collind_boot, baseline_year) {
725729
rate = if(nrow(point_est) > 0) point_est$rate else NA,
726730
rate_ci_lower = NA,
727731
rate_ci_upper = NA,
732+
rate_ci_80_lower = NA,
733+
rate_ci_80_upper = NA,
728734
pc1 = if(nrow(point_est) > 0) point_est$pc1 else NA,
729735
pc1_ci_lower = NA,
730736
pc1_ci_upper = NA,
737+
pc1_ci_80_lower = NA,
738+
pc1_ci_80_upper = NA,
731739
trend_class = "Uncertain"
732740
))
733741
}
734742

735-
rate_ci <- quantile(boot_only$rate, c(0.025, 0.975), na.rm = TRUE)
736-
pc1_ci <- quantile(boot_only$pc1, c(0.025, 0.975), na.rm = TRUE)
743+
# Calculate 95% CI (alpha = 0.05)
744+
rate_ci_95 <- quantile(boot_only$rate, c(0.025, 0.975), na.rm = TRUE)
745+
pc1_ci_95 <- quantile(boot_only$pc1, c(0.025, 0.975), na.rm = TRUE)
737746

738-
# Classify trend based on rate CI bounds
739-
ci_lower <- rate_ci[1]
740-
ci_upper <- rate_ci[2]
747+
# Calculate 80% CI (alpha = 0.20)
748+
rate_ci_80 <- quantile(boot_only$rate, c(0.10, 0.90), na.rm = TRUE)
749+
pc1_ci_80 <- quantile(boot_only$pc1, c(0.10, 0.90), na.rm = TRUE)
750+
751+
# Classify trend based on 95% rate CI bounds
752+
ci_lower <- rate_ci_95[1]
753+
ci_upper <- rate_ci_95[2]
741754

742755
trend_class <- if (ci_lower > 1.05) {
743756
"Strong increase"
@@ -756,14 +769,20 @@ calculate_trend_with_ci <- function(collind_boot, baseline_year) {
756769
cat(paste(" Trend classification:", trend_class, "\n"))
757770
cat(paste(" Annual rate:", round(if(nrow(point_est) > 0) point_est$rate else NA, 4), "\n"))
758771
cat(paste(" Annual % change:", round(if(nrow(point_est) > 0) point_est$pc1 else NA, 2), "%\n"))
772+
cat(paste(" 95% CI:", round(pc1_ci_95[1], 2), "to", round(pc1_ci_95[2], 2), "%\n"))
773+
cat(paste(" 80% CI:", round(pc1_ci_80[1], 2), "to", round(pc1_ci_80[2], 2), "%\n"))
759774

760775
return(list(
761776
rate = if(nrow(point_est) > 0) round(point_est$rate, 4) else NA,
762-
rate_ci_lower = round(rate_ci[1], 4),
763-
rate_ci_upper = round(rate_ci[2], 4),
777+
rate_ci_lower = round(rate_ci_95[1], 4),
778+
rate_ci_upper = round(rate_ci_95[2], 4),
779+
rate_ci_80_lower = round(rate_ci_80[1], 4),
780+
rate_ci_80_upper = round(rate_ci_80[2], 4),
764781
pc1 = if(nrow(point_est) > 0) round(point_est$pc1, 2) else NA,
765-
pc1_ci_lower = round(pc1_ci[1], 2),
766-
pc1_ci_upper = round(pc1_ci[2], 2),
782+
pc1_ci_lower = round(pc1_ci_95[1], 2),
783+
pc1_ci_upper = round(pc1_ci_95[2], 2),
784+
pc1_ci_80_lower = round(pc1_ci_80[1], 2),
785+
pc1_ci_80_upper = round(pc1_ci_80[2], 2),
767786
trend_class = trend_class
768787
))
769788
}
@@ -793,9 +812,13 @@ if (exists("collated_result_all") && !is.null(collated_result_all) && nrow(colla
793812
rate = NA,
794813
rate_ci_lower = NA,
795814
rate_ci_upper = NA,
815+
rate_ci_80_lower = NA,
816+
rate_ci_80_upper = NA,
796817
pc1 = NA,
797818
pc1_ci_lower = NA,
798819
pc1_ci_upper = NA,
820+
pc1_ci_80_lower = NA,
821+
pc1_ci_80_upper = NA,
799822
trend_class = "Uncertain"
800823
)
801824
}

src/components/ButterflyTransects.tsx

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect, useMemo } from "react";
2-
import { Link } from "react-router-dom";
2+
import { Link, useNavigate } from "react-router-dom";
33
import {
44
Table,
55
Input,
@@ -29,11 +29,10 @@ import {
2929
WarningOutlined,
3030
SettingOutlined,
3131
} from "@ant-design/icons";
32-
import { groupSpeciesByFamily } from "../constants";
32+
import { groupSpeciesByFamily, getTrendColor } from "../constants";
3333
import TransectMap from "./TransectMap";
3434
import TransectTimeline from "./TransectTimeline";
3535
import SpeciesLink from "./SpeciesLink";
36-
import GrasslandButterflyIndex from "./charts/GrasslandButterflyIndex";
3736
import SpeciesTrendsSparklines from "./charts/SpeciesTrendsSparklines";
3837
import MunicipalitySpeciesMap from "./charts/MunicipalitySpeciesMap";
3938

@@ -194,6 +193,7 @@ function ButterflyTransects() {
194193
const [protectedAreaFilters, setProtectedAreaFilters] = useState<string[]>([]);
195194
const [pageSize, setPageSize] = useState(20);
196195
const [currentPage, setCurrentPage] = useState(1);
196+
const navigate = useNavigate();
197197

198198
// Timeline data state
199199
const [timelineData, setTimelineData] = useState<TimelineData | null>(null);
@@ -682,7 +682,7 @@ function ButterflyTransects() {
682682

683683
{/* Summary Statistics */}
684684
<Row gutter={16}>
685-
<Col span={5}>
685+
<Col span={4}>
686686
<Card>
687687
<Popover
688688
content={
@@ -757,7 +757,7 @@ function ButterflyTransects() {
757757
</Popover>
758758
</Card>
759759
</Col>
760-
<Col span={5}>
760+
<Col span={4}>
761761
<Card>
762762
<Popover
763763
content={
@@ -894,13 +894,28 @@ function ButterflyTransects() {
894894
/>
895895
</Card>
896896
</Col>
897+
<Col span={4}>
898+
<Card hoverable style={{ cursor: "pointer" }} onClick={() => navigate("/gbi")}>
899+
<Statistic
900+
title="Tendência GBI"
901+
value={
902+
gbiLoading
903+
? "..."
904+
: gbiData?.gbiTrend?.pc1
905+
? `${gbiData.gbiTrend.pc1.toFixed(1)}%`
906+
: "N/A"
907+
}
908+
valueStyle={{
909+
color: gbiData?.gbiTrend?.category
910+
? getTrendColor(gbiData.gbiTrend.category)
911+
: "#8c8c8c",
912+
}}
913+
suffix={gbiData?.gbiTrend?.pc1 ? "/ano" : ""}
914+
/>
915+
</Card>
916+
</Col>
897917
</Row>
898918

899-
{/* Grassland Butterfly Index Chart */}
900-
<div style={{ marginTop: 24 }}>
901-
<GrasslandButterflyIndex gbiData={gbiData} loading={gbiLoading} />
902-
</div>
903-
904919
{/* Species Trends Sparklines */}
905920
<div style={{ marginTop: 24 }}>
906921
<SpeciesTrendsSparklines />

0 commit comments

Comments
 (0)