Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -689,10 +689,10 @@ export default function App(){

// ---- lazy-load ecoregion geojson + L3 yields when a map/AOI tool needs them ----
useEffect(()=>{
if(!(ecoOn || inspectMode || aoi)) return;
if(!ecoGeo) j("geo/us_eco_l3_features.geojson").then(setEcoGeo).catch(()=>{});
if(!(ecoOn || inspectMode || aoi || tab==="ecoharvest")) return;
if((ecoOn||inspectMode||aoi||tab==="ecoharvest") && !ecoGeo) j("geo/us_eco_l3_features.geojson").then(setEcoGeo).catch(()=>{});
if(!l3yields) j("api/yield_curves_by_l3.json").then(setL3yields).catch(()=>{});
},[ecoOn,inspectMode,aoi,ecoGeo,l3yields]);
},[ecoOn,inspectMode,aoi,ecoGeo,l3yields,tab]);

// ---- carbon-map year animation (play/pause) ----
useEffect(()=>{
Expand Down Expand Up @@ -1430,7 +1430,7 @@ export default function App(){
{(!aoi || researchOpen) && tab==="landis" && <LandisStratified data={landis} state={sel}/>}
{(!aoi || researchOpen) && tab==="landowner" && <LandownerYields data={landowner} state={sel}/>}
{(!aoi || researchOpen) && tab==="faustmann" && <FaustmannRotation data={faustmann} state={sel}/>}
{(!aoi || researchOpen) && tab==="ecoharvest" && <EcoregionHarvest data={ecoHarvest}/>}
{(!aoi || researchOpen) && tab==="ecoharvest" && <EcoregionHarvest data={ecoHarvest} geo={ecoGeo}/>}
{(!aoi || researchOpen) && (tab==="engines"||tab==="rd") && (<>
{LANDIS_STATES.includes(sel) && (
<div className="controls" style={{margin:"0 4px 8px"}}>
Expand Down
23 changes: 22 additions & 1 deletion src/EcoregionHarvest.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// the CONUS harvest-probability rasters. A sortable, searchable table — the
// "summarize by ecoregion" companion to the per-pixel raster overlays.
import { useMemo, useState } from "react";
import EcoregionMap from "./EcoregionMap.jsx";

const PCOLS = [
["p_harvest_any","P(any harvest)"],
Expand All @@ -27,7 +28,14 @@ const shade = v => {
return `rgba(${c[0]},${c[1]},${c[2]},0.55)`;
};

export default function EcoregionHarvest({ data }){
export default function EcoregionHarvest({ data, geo }){
const MAPFLDS=[["p_harvest_clearcut","P(stand replacement)",v=>v.toFixed(2)],
["p_harvest_any","P(any harvest)",v=>v.toFixed(2)],
["stand_height_ft","Stand height (ft)",v=>v.toFixed(0)],
["cspi_productivity","Productivity (CSPI)",v=>v.toFixed(0)],
["qmd_in","QMD (in)",v=>v.toFixed(1)]];
const [mapField,setMapField]=useState("p_harvest_clearcut");
const mf=MAPFLDS.find(x=>x[0]===mapField)||MAPFLDS[0];
const [sortKey,setSortKey]=useState("p_harvest_any");
const [asc,setAsc]=useState(false);
const [q,setQ]=useState("");
Expand All @@ -53,6 +61,19 @@ export default function EcoregionHarvest({ data }){
<b style={{fontSize:13}}>Forest summary by ecoregion</b>
<span style={{color:"var(--mut)",fontSize:11}}>{rows.length} EPA Level III ecoregions · ~3.1 km zonal mean · harvest probability + structure + productivity</span>
</div>
{data && data.ecoregions && geo && (
<div style={{marginBottom:8}}>
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:3,flexWrap:"wrap"}}>
<span style={{fontSize:11,color:"var(--mut)"}}>map:</span>
<select value={mapField} onChange={e=>setMapField(e.target.value)}
style={{background:"var(--panel,#172029)",color:"var(--fg,#e8eef2)",border:"1px solid var(--line,#2a3a47)",borderRadius:5,fontSize:11.5,padding:"2px 6px"}}>
{MAPFLDS.filter(x=>Object.values(data.ecoregions).some(v=>v[x[0]]!=null)).map(x=>
<option key={x[0]} value={x[0]}>{x[1]}</option>)}
</select>
</div>
<EcoregionMap geo={geo} eco={data.ecoregions} field={mapField} label={mf[1]} fmt={mf[2]}/>
</div>
)}
<input value={q} onChange={e=>setQ(e.target.value)} placeholder="filter by ecoregion or biome…"
style={{width:"min(320px,90%)",padding:"4px 8px",marginBottom:6,fontSize:12,
background:"var(--panel,#172029)",color:"var(--fg,#e8eef2)",border:"1px solid var(--line,#2a3a47)",borderRadius:5}}/>
Expand Down
51 changes: 51 additions & 0 deletions src/EcoregionMap.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Compact CONUS choropleth of an ecoregion-summary field (EPA L3 polygons).
// Self-contained Albers projection (mirrors SVGMap/PermanenceMap) so the
// ecoregion tab can show its summary spatially without the full map stack.
const W=640,H=400,PAD=8;
const PHI0=37.5*Math.PI/180,PHI1=29.5*Math.PI/180,PHI2=45.5*Math.PI/180,LAM0=-96*Math.PI/180;
const N=(Math.sin(PHI1)+Math.sin(PHI2))/2, C=Math.cos(PHI1)**2+2*N*Math.sin(PHI1), RHO0=Math.sqrt(C-2*N*Math.sin(PHI0))/N;
function project(lon,lat){ const phi=lat*Math.PI/180,lam=lon*Math.PI/180;
const rho=Math.sqrt(Math.max(0,C-2*N*Math.sin(phi)))/N, theta=N*(lam-LAM0);
return [rho*Math.sin(theta), RHO0-rho*Math.cos(theta)]; }
const _c=[[-125,50],[-66,50],[-125,24],[-66,24],[-95,49],[-95,25]].map(p=>project(...p));
const _xs=_c.map(c=>c[0]),_ys=_c.map(c=>c[1]);
const _x0=Math.min(..._xs),_x1=Math.max(..._xs),_y0=Math.min(..._ys),_y1=Math.max(..._ys),_dx=_x1-_x0,_dy=_y1-_y0;
const SCALE=Math.min((W-2*PAD)/_dx,(H-2*PAD)/_dy), TX=-_x0*SCALE+(W-_dx*SCALE)/2, TY=PAD+SCALE*_y1+(H-2*PAD-_dy*SCALE)/2;
const projPath=(lon,lat)=>{ const [x,y]=project(lon,lat); return [x*SCALE+TX,-y*SCALE+TY]; };
const ringToD=r=>{ let d=""; for(let i=0;i<r.length;i++){ const [x,y]=projPath(r[i][0],r[i][1]); d+=(i?"L":"M")+x.toFixed(1)+" "+y.toFixed(1);} return d+"Z"; };
const geomToD=g=>{ if(!g) return ""; const polys=g.type==="Polygon"?[g.coordinates]:g.coordinates; return polys.map(p=>p.map(ringToD).join(" ")).join(" "); };
// sequential blue->amber->red, value normalized 0..1
const STOPS=[[0,[47,98,158]],[0.5,[202,161,90]],[1,[224,90,90]]];
const ramp=t=>{ if(t==null||isNaN(t)) return "#2a3a47"; t=Math.max(0,Math.min(1,t));
let a=STOPS[0],b=STOPS[STOPS.length-1];
for(let i=1;i<STOPS.length;i++){ if(t<=STOPS[i][0]){ a=STOPS[i-1]; b=STOPS[i]; break; } }
const f=(t-a[0])/((b[0]-a[0])||1); const c=a[1].map((ca,k)=>Math.round(ca+f*(b[1][k]-ca)));
return "#"+c.map(x=>x.toString(16).padStart(2,"0")).join(""); };

export default function EcoregionMap({ geo, eco, field, label, fmt }){
if(!geo || !geo.features || !eco) return <div style={{color:"var(--mut)",fontSize:11,padding:"6px 0"}}>Loading ecoregion map…</div>;
const feats=geo.features.filter(ft=>ft.properties && ft.properties.NA_L3CODE);
const vals=Object.values(eco).map(v=>v[field]).filter(v=>v!=null&&isFinite(v));
const lo=Math.min(...vals), hi=Math.max(...vals), span=(hi-lo)||1;
return (
<div>
<svg viewBox={`0 0 ${W} ${H}`} style={{width:"100%",height:"auto",display:"block"}}>
<rect width={W} height={H} fill="#101820"/>
{feats.map((ft,i)=>{
const code=ft.properties.NA_L3CODE; const r=eco[code]; const v=r?r[field]:null;
const t=(v!=null&&isFinite(v))?(v-lo)/span:null;
return <path key={code+i} d={geomToD(ft.geometry)} fill={ramp(t)} fillOpacity={v!=null?0.92:0.18}
stroke="#0b1015" strokeWidth="0.3">
<title>{`${r?r.name:code}${v!=null?` · ${label}: ${fmt?fmt(v):v}`:" · no data"}`}</title>
</path>;
})}
<g transform={`translate(${W-150},${H-30})`}>
{[0,0.25,0.5,0.75,1].map((t,i)=>(<rect key={i} x={i*26} y={0} width={26} height={9} fill={ramp(t)}/>))}
<text x={0} y={22} fill="#8aa0b0" fontSize="9">{fmt?fmt(lo):lo.toFixed(1)}</text>
<text x={130} y={22} textAnchor="end" fill="#8aa0b0" fontSize="9">{fmt?fmt(hi):hi.toFixed(1)}</text>
</g>
<text x={12} y={20} fill="#cddbe4" fontSize="13" fontWeight="bold">{label} by ecoregion</text>
</svg>
</div>
);
}