diff --git a/public/api/ecoregion_harvest_summary.json b/public/api/ecoregion_harvest_summary.json new file mode 100644 index 0000000..08e4199 --- /dev/null +++ b/public/api/ecoregion_harvest_summary.json @@ -0,0 +1 @@ +{"meta": {"source": "conus_render ph_*.tif harvest probability (TreeMap-based, ~3.1km Albers) zonal mean by EPA L3 ecoregion", "n_ecoregions": 74, "fields": ["p_harvest_any", "p_harvest_clearcut", "p_harvest_partial"], "note": "Per-EPA-Level-III-ecoregion mean harvest probability. P(any), P(stand-replacement), P(partial)."}, "ecoregions": {"7.1.8": {"name": "Coast Range", "l1": "MARINE WEST COAST FOREST", "npix": 88, "p_harvest_any": 0.6855, "p_harvest_clearcut": 0.6664, "p_harvest_partial": 0.4093}, "10.1.2": {"name": "Columbia Plateau", "l1": "NORTH AMERICAN DESERTS", "npix": 237, "p_harvest_any": 0.7404, "p_harvest_clearcut": 0.6895, "p_harvest_partial": 0.6791}, "6.2.9": {"name": "Blue Mountains", "l1": "NORTHWESTERN FORESTED MOUNTAINS", "npix": 1995, "p_harvest_any": 0.8558, "p_harvest_clearcut": 0.7079, "p_harvest_partial": 0.5734}, "10.1.8": {"name": "Snake River Plain", "l1": "NORTH AMERICAN DESERTS", "npix": 52, "p_harvest_any": 0.8771, "p_harvest_clearcut": 0.6755, "p_harvest_partial": 0.6054}, "10.1.5": {"name": "Central Basin and Range", "l1": "NORTH AMERICAN DESERTS", "npix": 376, "p_harvest_any": 0.8527, "p_harvest_clearcut": 0.6661, "p_harvest_partial": 0.649}, "6.2.3": {"name": "Columbia Mountains/Northern Rockies", "l1": "NORTHWESTERN FORESTED MOUNTAINS", "npix": 5482, "p_harvest_any": 0.891, "p_harvest_clearcut": 0.7376, "p_harvest_partial": 0.6727}, "6.2.15": {"name": "Idaho Batholith", "l1": "NORTHWESTERN FORESTED MOUNTAINS", "npix": 3474, "p_harvest_any": 0.8972, "p_harvest_clearcut": 0.8046, "p_harvest_partial": 0.3389}, "6.2.10": {"name": "Middle Rockies", "l1": "NORTHWESTERN FORESTED MOUNTAINS", "npix": 3376, "p_harvest_any": 0.8746, "p_harvest_clearcut": 0.6936, "p_harvest_partial": 0.5571}, "10.1.4": {"name": "Wyoming Basin", "l1": "NORTH AMERICAN DESERTS", "npix": 83, "p_harvest_any": 0.8839, "p_harvest_clearcut": 0.6822, "p_harvest_partial": 0.6237}, "6.2.13": {"name": "Wasatch and Uinta Mountains", "l1": "NORTHWESTERN FORESTED MOUNTAINS", "npix": 766, "p_harvest_any": 0.8777, "p_harvest_clearcut": 0.7222, "p_harvest_partial": 0.5169}, "7.1.7": {"name": "Strait of Georgia/Puget Lowland", "l1": "MARINE WEST COAST FOREST", "npix": 29, "p_harvest_any": 0.9203, "p_harvest_clearcut": 0.7488, "p_harvest_partial": 0.6828}, "10.1.6": {"name": "Colorado Plateaus", "l1": "NORTH AMERICAN DESERTS", "npix": 68, "p_harvest_any": 0.8909, "p_harvest_clearcut": 0.7392, "p_harvest_partial": 0.563}, "6.2.14": {"name": "Southern Rockies", "l1": "NORTHWESTERN FORESTED MOUNTAINS", "npix": 375, "p_harvest_any": 0.8932, "p_harvest_clearcut": 0.7705, "p_harvest_partial": 0.4755}, "9.4.2": {"name": "Central Great Plains", "l1": "GREAT PLAINS", "npix": 4589, "p_harvest_any": 0.7917, "p_harvest_clearcut": 0.7176, "p_harvest_partial": 0.4446}, "9.4.4": {"name": "Flint Hills", "l1": "GREAT PLAINS", "npix": 1677, "p_harvest_any": 0.7666, "p_harvest_clearcut": 0.7038, "p_harvest_partial": 0.4813}, "9.4.5": {"name": "Cross Timbers", "l1": "GREAT PLAINS", "npix": 5828, "p_harvest_any": 0.8277, "p_harvest_clearcut": 0.7258, "p_harvest_partial": 0.443}, "9.4.6": {"name": "Edwards Plateau", "l1": "GREAT PLAINS", "npix": 102, "p_harvest_any": 0.815, "p_harvest_clearcut": 0.7086, "p_harvest_partial": 0.3777}, "9.6.1": {"name": "Southern Texas Plains/Interior Plains and Hills with Xerophytic Shrub and Oak Forest", "l1": "GREAT PLAINS", "npix": 54, "p_harvest_any": 0.8578, "p_harvest_clearcut": 0.7951, "p_harvest_partial": 0.3}, "9.4.7": {"name": "Texas Blackland Prairies", "l1": "GREAT PLAINS", "npix": 2774, "p_harvest_any": 0.8338, "p_harvest_clearcut": 0.7447, "p_harvest_partial": 0.4065}, "8.3.8": {"name": "East Central Texas Plains", "l1": "EASTERN TEMPERATE FORESTS", "npix": 2789, "p_harvest_any": 0.834, "p_harvest_clearcut": 0.7589, "p_harvest_partial": 0.4062}, "9.5.1": {"name": "Western Gulf Coastal Plain", "l1": "GREAT PLAINS", "npix": 3788, "p_harvest_any": 0.8427, "p_harvest_clearcut": 0.7639, "p_harvest_partial": 0.3785}, "8.3.7": {"name": "South Central Plains", "l1": "EASTERN TEMPERATE FORESTS", "npix": 15297, "p_harvest_any": 0.8493, "p_harvest_clearcut": 0.769, "p_harvest_partial": 0.4243}, "8.4.8": {"name": "Ouachita Mountains", "l1": "EASTERN TEMPERATE FORESTS", "npix": 2702, "p_harvest_any": 0.841, "p_harvest_clearcut": 0.7618, "p_harvest_partial": 0.4422}, "8.4.7": {"name": "Arkansas Valley", "l1": "EASTERN TEMPERATE FORESTS", "npix": 2850, "p_harvest_any": 0.8362, "p_harvest_clearcut": 0.7444, "p_harvest_partial": 0.4451}, "8.4.6": {"name": "Boston Mountains", "l1": "EASTERN TEMPERATE FORESTS", "npix": 1433, "p_harvest_any": 0.8057, "p_harvest_clearcut": 0.742, "p_harvest_partial": 0.4419}, "8.4.5": {"name": "Ozark Highlands", "l1": "EASTERN TEMPERATE FORESTS", "npix": 9059, "p_harvest_any": 0.7961, "p_harvest_clearcut": 0.7167, "p_harvest_partial": 0.4513}, "6.2.7": {"name": "Cascades", "l1": "NORTHWESTERN FORESTED MOUNTAINS", "npix": 1161, "p_harvest_any": 0.8422, "p_harvest_clearcut": 0.7169, "p_harvest_partial": 0.5839}, "9.2.4": {"name": "Central Irregular Plains", "l1": "GREAT PLAINS", "npix": 8270, "p_harvest_any": 0.8148, "p_harvest_clearcut": 0.7092, "p_harvest_partial": 0.503}, "6.2.4": {"name": "Canadian Rockies", "l1": "NORTHWESTERN FORESTED MOUNTAINS", "npix": 1038, "p_harvest_any": 0.8973, "p_harvest_clearcut": 0.7032, "p_harvest_partial": 0.66}, "9.3.1": {"name": "Northwestern Glaciated Plains", "l1": "GREAT PLAINS", "npix": 298, "p_harvest_any": 0.8563, "p_harvest_clearcut": 0.6851, "p_harvest_partial": 0.5549}, "9.3.3": {"name": "Northwestern Great Plains", "l1": "GREAT PLAINS", "npix": 239, "p_harvest_any": 0.8825, "p_harvest_clearcut": 0.6672, "p_harvest_partial": 0.6462}, "8.3.4": {"name": "Piedmont", "l1": "EASTERN TEMPERATE FORESTS", "npix": 16733, "p_harvest_any": 0.8306, "p_harvest_clearcut": 0.7636, "p_harvest_partial": 0.4697}, "9.2.1": {"name": "Aspen Parkland/Northern Glaciated Plains", "l1": "GREAT PLAINS", "npix": 4548, "p_harvest_any": 0.8847, "p_harvest_clearcut": 0.6826, "p_harvest_partial": 0.6471}, "9.2.3": {"name": "Western Corn Belt Plains", "l1": "GREAT PLAINS", "npix": 20716, "p_harvest_any": 0.8587, "p_harvest_clearcut": 0.7203, "p_harvest_partial": 0.6268}, "9.2.2": {"name": "Lake Manitoba and Lake Agassiz Plain", "l1": "GREAT PLAINS", "npix": 1407, "p_harvest_any": 0.8934, "p_harvest_clearcut": 0.6899, "p_harvest_partial": 0.6577}, "5.2.2": {"name": "Northern Minnesota Wetlands", "l1": "NORTHERN FORESTS", "npix": 35, "p_harvest_any": 0.8999, "p_harvest_clearcut": 0.7103, "p_harvest_partial": 0.6537}, "6.2.12": {"name": "Sierra Nevada", "l1": "NORTHWESTERN FORESTED MOUNTAINS", "npix": 1589, "p_harvest_any": 0.8563, "p_harvest_clearcut": 0.7299, "p_harvest_partial": 0.4779}, "5.2.1": {"name": "Northern Lakes and Forests", "l1": "NORTHERN FORESTS", "npix": 14416, "p_harvest_any": 0.8949, "p_harvest_clearcut": 0.7292, "p_harvest_partial": 0.653}, "8.1.4": {"name": "North Central Hardwood Forests", "l1": "EASTERN TEMPERATE FORESTS", "npix": 8798, "p_harvest_any": 0.8987, "p_harvest_clearcut": 0.7313, "p_harvest_partial": 0.6516}, "8.1.5": {"name": "Driftless Area", "l1": "EASTERN TEMPERATE FORESTS", "npix": 4774, "p_harvest_any": 0.8901, "p_harvest_clearcut": 0.7237, "p_harvest_partial": 0.6192}, "8.2.1": {"name": "Southeastern Wisconsin Till Plains", "l1": "EASTERN TEMPERATE FORESTS", "npix": 3153, "p_harvest_any": 0.8965, "p_harvest_clearcut": 0.7276, "p_harvest_partial": 0.6377}, "8.2.3": {"name": "Central Corn Belt Plains", "l1": "EASTERN TEMPERATE FORESTS", "npix": 5177, "p_harvest_any": 0.8476, "p_harvest_clearcut": 0.7049, "p_harvest_partial": 0.6213}, "8.2.4": {"name": "Eastern Corn Belt Plains", "l1": "EASTERN TEMPERATE FORESTS", "npix": 6581, "p_harvest_any": 0.8161, "p_harvest_clearcut": 0.7128, "p_harvest_partial": 0.5754}, "8.1.6": {"name": "Southern Michigan/Northern Indiana Drift Plains", "l1": "EASTERN TEMPERATE FORESTS", "npix": 5367, "p_harvest_any": 0.8784, "p_harvest_clearcut": 0.7273, "p_harvest_partial": 0.631}, "8.2.2": {"name": "Huron/Erie Lake Plains", "l1": "EASTERN TEMPERATE FORESTS", "npix": 3141, "p_harvest_any": 0.8925, "p_harvest_clearcut": 0.7286, "p_harvest_partial": 0.6327}, "5.3.1": {"name": "Northern Appalachian and Atlantic Maritime Highlands", "l1": "NORTHERN FORESTS", "npix": 8678, "p_harvest_any": 0.8751, "p_harvest_clearcut": 0.7145, "p_harvest_partial": 0.636}, "8.1.7": {"name": "Northeastern Coastal Zone", "l1": "EASTERN TEMPERATE FORESTS", "npix": 4088, "p_harvest_any": 0.8314, "p_harvest_clearcut": 0.6941, "p_harvest_partial": 0.6348}, "11.1.1": {"name": "California Coastal Sage, Chaparral, and Oak Woodlands", "l1": "MEDITERRANEAN CALIFORNIA", "npix": 1254, "p_harvest_any": 0.8298, "p_harvest_clearcut": 0.7104, "p_harvest_partial": 0.415}, "8.1.3": {"name": "Northern Allegheny Plateau", "l1": "EASTERN TEMPERATE FORESTS", "npix": 4686, "p_harvest_any": 0.8555, "p_harvest_clearcut": 0.7255, "p_harvest_partial": 0.6347}, "8.1.10": {"name": "Erie Drift Plain", "l1": "EASTERN TEMPERATE FORESTS", "npix": 3090, "p_harvest_any": 0.792, "p_harvest_clearcut": 0.6875, "p_harvest_partial": 0.6268}, "5.3.3": {"name": "North Central Appalachians", "l1": "NORTHERN FORESTS", "npix": 2652, "p_harvest_any": 0.8505, "p_harvest_clearcut": 0.71, "p_harvest_partial": 0.6362}, "8.5.1": {"name": "Middle Atlantic Coastal Plain", "l1": "EASTERN TEMPERATE FORESTS", "npix": 7539, "p_harvest_any": 0.8651, "p_harvest_clearcut": 0.7681, "p_harvest_partial": 0.4681}, "8.3.1": {"name": "Northern Piedmont", "l1": "EASTERN TEMPERATE FORESTS", "npix": 2904, "p_harvest_any": 0.807, "p_harvest_clearcut": 0.6964, "p_harvest_partial": 0.4704}, "8.3.5": {"name": "Southeastern Plains", "l1": "EASTERN TEMPERATE FORESTS", "npix": 33192, "p_harvest_any": 0.8554, "p_harvest_clearcut": 0.7682, "p_harvest_partial": 0.4411}, "8.4.4": {"name": "Blue Ridge", "l1": "EASTERN TEMPERATE FORESTS", "npix": 4678, "p_harvest_any": 0.8474, "p_harvest_clearcut": 0.7787, "p_harvest_partial": 0.4121}, "8.4.1": {"name": "Ridge and Valley", "l1": "EASTERN TEMPERATE FORESTS", "npix": 11353, "p_harvest_any": 0.8237, "p_harvest_clearcut": 0.7367, "p_harvest_partial": 0.4904}, "8.4.9": {"name": "Southwestern Appalachians", "l1": "EASTERN TEMPERATE FORESTS", "npix": 3825, "p_harvest_any": 0.8398, "p_harvest_clearcut": 0.7628, "p_harvest_partial": 0.4007}, "8.4.2": {"name": "Central Appalachians", "l1": "EASTERN TEMPERATE FORESTS", "npix": 6150, "p_harvest_any": 0.8249, "p_harvest_clearcut": 0.7352, "p_harvest_partial": 0.4817}, "11.1.2": {"name": "Central California Valley", "l1": "MEDITERRANEAN CALIFORNIA", "npix": 13, "p_harvest_any": 0.8438, "p_harvest_clearcut": 0.7218, "p_harvest_partial": 0.4405}, "8.4.3": {"name": "Western Allegheny Plateau", "l1": "EASTERN TEMPERATE FORESTS", "npix": 7825, "p_harvest_any": 0.8212, "p_harvest_clearcut": 0.7226, "p_harvest_partial": 0.5033}, "8.3.3": {"name": "Interior Plateau", "l1": "EASTERN TEMPERATE FORESTS", "npix": 12006, "p_harvest_any": 0.8399, "p_harvest_clearcut": 0.7461, "p_harvest_partial": 0.4433}, "8.3.2": {"name": "Interior River Valleys and Hills", "l1": "EASTERN TEMPERATE FORESTS", "npix": 7524, "p_harvest_any": 0.7914, "p_harvest_clearcut": 0.7141, "p_harvest_partial": 0.5064}, "8.5.2": {"name": "Mississippi Alluvial Plain", "l1": "EASTERN TEMPERATE FORESTS", "npix": 10875, "p_harvest_any": 0.8505, "p_harvest_clearcut": 0.7645, "p_harvest_partial": 0.4141}, "8.3.6": {"name": "Mississippi Valley Loess Plains", "l1": "EASTERN TEMPERATE FORESTS", "npix": 5232, "p_harvest_any": 0.8519, "p_harvest_clearcut": 0.7706, "p_harvest_partial": 0.4112}, "8.5.3": {"name": "Southern Coastal Plain", "l1": "EASTERN TEMPERATE FORESTS", "npix": 14281, "p_harvest_any": 0.8407, "p_harvest_clearcut": 0.778, "p_harvest_partial": 0.3827}, "15.4.1": {"name": "Southern Florida Coastal Plain", "l1": "TROPICAL WET FORESTS", "npix": 2127, "p_harvest_any": 0.8369, "p_harvest_clearcut": 0.7807, "p_harvest_partial": 0.3188}, "6.2.5": {"name": "North Cascades", "l1": "NORTHWESTERN FORESTED MOUNTAINS", "npix": 1072, "p_harvest_any": 0.8669, "p_harvest_clearcut": 0.6875, "p_harvest_partial": 0.649}, "6.2.11": {"name": "Klamath Mountains", "l1": "NORTHWESTERN FORESTED MOUNTAINS", "npix": 125, "p_harvest_any": 0.9018, "p_harvest_clearcut": 0.8752, "p_harvest_partial": 0.1314}, "11.1.3": {"name": "Southern and Baja California Pine-Oak Mountains", "l1": "MEDITERRANEAN CALIFORNIA", "npix": 6, "p_harvest_any": 0.7345, "p_harvest_clearcut": 0.6701, "p_harvest_partial": 0.1953}, "10.1.3": {"name": "Northern Basin and Range", "l1": "NORTH AMERICAN DESERTS", "npix": 672, "p_harvest_any": 0.8724, "p_harvest_clearcut": 0.664, "p_harvest_partial": 0.6511}, "8.1.8": {"name": "Acadian Plains and Hills", "l1": "EASTERN TEMPERATE FORESTS", "npix": 3578, "p_harvest_any": 0.8829, "p_harvest_clearcut": 0.7008, "p_harvest_partial": 0.6514}, "8.1.1": {"name": "Eastern Great Lakes Lowlands", "l1": "EASTERN TEMPERATE FORESTS", "npix": 4094, "p_harvest_any": 0.8857, "p_harvest_clearcut": 0.7243, "p_harvest_partial": 0.6443}, "8.5.4": {"name": "Atlantic Coastal Pine Barrens", "l1": "EASTERN TEMPERATE FORESTS", "npix": 1139, "p_harvest_any": 0.8296, "p_harvest_clearcut": 0.7213, "p_harvest_partial": 0.5137}, "6.2.8": {"name": "Eastern Cascades Slopes and Foothills", "l1": "NORTHWESTERN FORESTED MOUNTAINS", "npix": 1100, "p_harvest_any": 0.8505, "p_harvest_clearcut": 0.7409, "p_harvest_partial": 0.5059}}} \ No newline at end of file diff --git a/scripts/build_ecoregion_harvest_summary.py b/scripts/build_ecoregion_harvest_summary.py new file mode 100644 index 0000000..ceddd74 --- /dev/null +++ b/scripts/build_ecoregion_harvest_summary.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Zonal-mean the CONUS harvest-probability rasters by EPA Level III ecoregion. + +Inputs (on Cardinal, ~/conus_render/): ph_any.tif, ph_clearcut.tif, +ph_partial.tif — single-band float32 P(harvest) at ~3.1 km Albers, plus the +EPA L3 polygons (public/geo/us_eco_l3_features.geojson, WGS84). +Output: public/api/ecoregion_harvest_summary.json (mean P by L3 code). + +Run: python3 scripts/build_ecoregion_harvest_summary.py (needs rasterio, pyproj) +The .tif inputs are not committed (each ~6.8 MB); pull from Cardinal first. +""" +import json, numpy as np, rasterio, sys +from rasterio.features import rasterize +from pyproj import Transformer +ALBERS="+proj=aea +lat_1=29.5 +lat_2=45.5 +lat_0=37.5 +lon_0=-96 +x_0=0 +y_0=0 +datum=NAD83 +units=m +no_defs" +GEO=sys.argv[1] if len(sys.argv)>1 else "public/geo/us_eco_l3_features.geojson" +tr=Transformer.from_crs("EPSG:4326",ALBERS,always_xy=True) +def reproj(g): + def rc(c): return [rc(x) if isinstance(x[0],(list,tuple)) else list(tr.transform(x[0],x[1])) for x in c] + return {"type":g["type"],"coordinates":rc(g["coordinates"])} +gj=json.load(open(GEO)); feats=gj["features"]; codes={}; meta={} +for f in feats: + p=f["properties"]; code=p.get("NA_L3CODE") + if code is None: continue + if code not in codes: codes[code]=len(codes)+1; meta[code]={"name":p.get("NA_L3NAME"),"l1":p.get("NA_L1NAME")} +shapes=[(reproj(f["geometry"]),codes[f["properties"]["NA_L3CODE"]]) for f in feats + if f["properties"].get("NA_L3CODE") and f.get("geometry")] +with rasterio.open("ph_any.tif") as r: transform=r.transform; H,W=r.height,r.width +zone=rasterize(shapes,out_shape=(H,W),transform=transform,fill=0,dtype="int32"); K=len(codes)+1 +fields={"p_harvest_any":"ph_any.tif","p_harvest_clearcut":"ph_clearcut.tif","p_harvest_partial":"ph_partial.tif"} +band={} +for key,fn in fields.items(): + with rasterio.open(fn) as r: a=r.read(1).astype("float64"); nod=r.nodata + valid=np.isfinite(a)&(zone>0) + if nod is not None: valid&=(a!=nod) + band[key]=(np.bincount(zone[valid],weights=a[valid],minlength=K),np.bincount(zone[valid],minlength=K)) +inv={v:k for k,v in codes.items()}; out={} +for i in range(1,K): + if band["p_harvest_any"][1][i]<5: continue + code=inv[i]; rec={"name":meta[code]["name"],"l1":meta[code]["l1"],"npix":int(band["p_harvest_any"][1][i])} + for key in fields: + s,cc=band[key]; rec[key]=round(float(s[i]/cc[i]),4) if cc[i]>0 else None + out[code]=rec +json.dump({"meta":{"source":"conus_render ph_*.tif harvest probability (TreeMap-based, ~3.1km Albers) zonal mean by EPA L3 ecoregion","n_ecoregions":len(out),"fields":list(fields.keys()),"note":"Per-EPA-Level-III-ecoregion mean harvest probability. P(any), P(stand-replacement), P(partial)."},"ecoregions":out},open("public/api/ecoregion_harvest_summary.json","w")) +print("wrote", len(out), "ecoregions") diff --git a/src/App.jsx b/src/App.jsx index add1dee..f869c95 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,6 +7,7 @@ import StumpagePanel from "./StumpagePanel.jsx"; import LandisStratified from "./LandisStratified.jsx"; import LandownerYields from "./LandownerYields.jsx"; import FaustmannRotation from "./FaustmannRotation.jsx"; +import EcoregionHarvest from "./EcoregionHarvest.jsx"; import AOIReport from "./AOIReport.jsx"; import { findFeature, agbAtAge, polygonCentroid, polygonAreaM2, pointInGeometry } from "./geo.js"; import { ownershipComposition, riskSummary, forestFraction, forestTypeDiversity, rampRelative, rampValues, median, percentile } from "./rasterSample.js"; @@ -471,6 +472,7 @@ export default function App(){ const [landis,setLandis] = useState(null); const [landowner,setLandowner] = useState(null); const [faustmann,setFaustmann] = useState(null); + const [ecoHarvest,setEcoHarvest] = useState(null); // v1.3 map/AOI tools const [ecoOn,setEcoOn] = useState(false); const [ecoGeo,setEcoGeo] = useState(null); @@ -499,6 +501,7 @@ export default function App(){ j("api/landis_stratified.json").then(setLandis).catch(()=>{}); j("api/landowner_yields.json").then(setLandowner).catch(()=>{}); j("api/faustmann_rotation.json").then(setFaustmann).catch(()=>{}); + j("api/ecoregion_harvest_summary.json").then(setEcoHarvest).catch(()=>{}); geo.features.forEach(ft=>{ const st=ft.properties.state; const c=s[st]; ft.properties.engines = c ? c.engines : 0; ft.properties.hasSeries = (c && c.has_series) ? 1 : 0; @@ -1403,12 +1406,13 @@ export default function App(){ {(!aoi || researchOpen) &&
| {r.code} | +{r.name} | +{(r.l1||"").toLowerCase().replace(/\b\w/g,c=>c.toUpperCase())} | + {COLS.map(([k])=>( ++ {r[k]!=null?r[k].toFixed(2):"—"} | ))} +