Download index.html from Hugdrigo/tradescope-export-intelligence-lab: direct link, hf CLI and curl.
- Browser
- Download file 46.8 kB
-
https://huggingface.co/spaces/Hugdrigo/tradescope-export-intelligence-lab/resolve/main/index.html
- Command line
-
hf download hf://spaces/Hugdrigo/tradescope-export-intelligence-lab/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Hugdrigo/tradescope-export-intelligence-lab/resolve/main/index.html
46.8 kB
| <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="TradeScope is an offline international trade analytics laboratory for export concentration, market stress and diversification."><title>TradeScope | Export Intelligence Lab</title><style> | |
| :root{color-scheme:dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#080e18;color:#eaf1f6;font-synthesis:none;font-feature-settings:"tnum";--canvas:#0b111c;--card:#101a27;--border:#273341;--quiet:#8191a6;--green:#37c9a9;--sky:#85aafa;--gold:#dfbd83}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;min-width:320px;background:#0b1019;color:#eaf1f6}button,input,select{font:inherit}button{cursor:pointer}button:focus-visible,select:focus-visible,input:focus-visible,a:focus-visible{outline:2px solid #71b1ff;outline-offset:3px}a{color:inherit;text-decoration:none}.shell{display:grid;grid-template-columns:260px minmax(0,1fr);min-height:100vh;max-width:1880px;margin:auto}.sidebar{background:#0b121d;border-right:1px solid #23303f;display:flex;flex-direction:column;padding:30px 15px 25px;min-height:100vh;position:sticky;top:0;height:100vh}.brand{display:flex;align-items:center;gap:12px;padding:4px 10px 46px}.brand-icon{width:41px;height:41px;border:1px solid #64caae;border-radius:12px;background:#173e37;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:800;color:#e5fff6;box-shadow:0 0 26px #1a6a5840}.brand-icon span{color:#48e0bc}.brand strong{display:block;font-size:20px;letter-spacing:-.6px}.period{color:var(--green)}.brand small{font-size:8px;letter-spacing:1.4px;color:#9da9b7}.side-label{font-size:10px;font-weight:750;letter-spacing:2px;color:#69798a;margin:0 15px 17px}.menu{display:grid;gap:7px}.nav-item{width:100%;background:none;border:1px solid transparent;color:#9cabbd;text-align:left;display:flex;align-items:center;gap:11px;border-radius:9px;padding:13px 11px;font-size:12px;font-weight:600}.nav-icon{width:24px;font-size:21px;line-height:20px;color:#899cb3;text-align:center}.nav-item.active{background:#162f2d;border:1px solid #276754;color:#e2fff4}.nav-item.active .nav-icon{color:#55dfb8}.nav-item i{font-size:6px;margin-left:auto;color:var(--green)}.nav-item:hover{background:#182635;color:white}.sidebar-bottom{margin-top:auto;padding:20px 11px 0}.status{font-size:11px;color:#b3c3ca;padding:13px 10px;border:1px solid #2b3b45;border-radius:8px;background:#15232d}.status i{display:inline-block;border-radius:50%;width:7px;height:7px;margin-right:8px;background:var(--green);box-shadow:0 0 10px var(--green)}.side-small{font-size:9px;color:#617284;letter-spacing:1px;line-height:2;margin-top:19px}.workspace{min-width:0}.topbar{height:73px;border-bottom:1px solid #253342;background:#0e1622;display:flex;align-items:center;justify-content:space-between;padding:0 39px}.breadcrumb{font-size:10px;color:#8294a6;letter-spacing:1.4px;font-weight:650}.breadcrumb span{color:#4f6372;margin:0 12px}.topright{display:flex;align-items:center;gap:21px}.top-tag{font-size:10px;color:var(--green);border:1px solid #31534a;padding:7px 10px;border-radius:5px;letter-spacing:.7px}.top-avatar{width:31px;height:31px;border-radius:8px;display:grid;place-items:center;background:#2c4159;font-size:10px;font-weight:800}.main{padding:38px 42px 30px;max-width:1510px;margin:auto}.intro{display:flex;justify-content:space-between;align-items:flex-end;gap:20px;margin-bottom:30px}.eyebrow{font-weight:750;letter-spacing:2.1px;color:#67cdb6;font-size:10px;display:flex;align-items:center;gap:10px}.eyebrow-line{height:1px;width:22px;background:var(--green)}.eyebrow-number{color:#60798d}.intro h1{font-size:35px;line-height:1.12;letter-spacing:-1.25px;margin:13px 0}.intro p{font-size:12px;color:#96a5b6;line-height:1.7;max-width:690px;margin:0}.dataset-control{min-width:242px;border:1px solid #2a3a48;background:#15202d;border-radius:9px;padding:12px}.dataset-control label{display:block;font-size:9px;color:#7c91a3;letter-spacing:1.4px;font-weight:700;margin:0 0 5px}.dataset-control select{font-size:12px;color:#e2eef2;background:#15202d;border:0;width:100%;font-weight:750;padding:4px 0}.dataset-control small{font-size:10px;color:#8296a4;display:block;padding-top:5px}.content{display:grid;gap:19px}.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:15px}.metric,.panel{border:1px solid #263544;background:linear-gradient(155deg,#152131,#101925);border-radius:11px;box-shadow:0 5px 24px #00000010}.metric{min-height:145px;padding:20px 19px}.metric-top{display:flex;justify-content:space-between;align-items:center;gap:9px;color:#a4b0c0;font-size:11px}.metric-icon{font-size:19px;color:#55ae9c}.metric>strong{font-size:28px;line-height:1.5;letter-spacing:-.7px;display:block;margin-top:12px;color:#f4fafb}.metric p{font-size:10px;color:#7f92a5;margin:3px 0}.two-col{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);gap:18px}.panel{min-width:0;padding:22px}.panel-head{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:20px}.panel h3{margin:0;font-size:14px;letter-spacing:.1px}.panel .panel-head p{font-size:11px;color:#8294a7;margin:7px 0 0;line-height:1.5}.donut-layout{display:grid;grid-template-columns:minmax(145px,210px) 1fr;align-items:center;gap:20px}.donut-layout svg{width:100%}.chart-big{font-size:37px;font-weight:750;fill:#f0f7f9}.chart-small{font-size:11px;fill:#8395a7}.legend{display:grid;gap:14px}.legend>div{display:grid;grid-template-columns:8px 1fr auto;align-items:center;gap:9px;font-size:11px;color:#a7b7c5}.legend i{height:8px;width:8px;border-radius:2px}.legend strong{font-weight:750;color:#e0eaf0}.bar-stack{display:grid;gap:15px}.bar-item{min-width:0}.bar-meta{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:7px;font-size:11px}.bar-meta strong{font-weight:600;color:#c3d3df;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bar-meta span{color:#b3c4d2;font-weight:700;white-space:nowrap}.bar-track{height:8px;width:100%;background:#293747;border-radius:8px;overflow:hidden}.bar-fill{height:100%;border-radius:8px;background:#37c9a9}.color-1{background:#82a1f8}.color-2{background:#d2b180}.color-3{background:#b48af2}.color-4{background:#78bdc8}.chart-header{display:flex;justify-content:space-between;align-items:center;gap:7px;margin-bottom:19px;font-size:11px}.quiet{color:#8a9caf!important}.detail-grid,.focus-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:13px}.detail-grid>div,.focus-grid>div{background:#111e2b;border:1px solid #293949;padding:16px;border-radius:7px;min-width:0}.detail-grid span,.focus-grid span{display:block;font-size:11px;color:#8fa2b3;margin-bottom:8px}.detail-grid strong,.focus-grid strong{display:block;font-size:21px;color:#e9f6f4}.help,.disclaimer{font-size:11px;line-height:1.7;color:#90a2b5;border-left:2px solid #406b67;padding-left:12px;margin-top:17px}.tablewrap{width:100%;overflow:auto}table{border-collapse:collapse;width:100%;font-size:11px;text-align:left;white-space:nowrap}th{font-size:10px;letter-spacing:.5px;text-transform:uppercase;color:#7b90a3;font-weight:750}td,th{padding:12px 9px;border-bottom:1px solid #273747}td{color:#bbcad5}td:first-child{font-weight:700;color:#e2ebef}.risk{font-weight:800;border-radius:4px;padding:4px 7px}.risk.low{background:#183d37;color:#75e6bf}.risk.medium{background:#3d3224;color:#e6bd7f}.risk.high{background:#422b31;color:#ff9eaa}.matrix-wrap{overflow:auto}.heatmap{display:grid;grid-template-columns:minmax(160px,1.45fr) repeat(var(--cols),minmax(115px,1fr));gap:6px;min-width:760px;align-items:stretch}.heat-head,.heat-market{font-size:10px;text-transform:uppercase;color:#8da3b6;letter-spacing:.7px;padding:10px 5px}.heat-product{display:flex;align-items:center;padding:10px 7px;font-weight:650;font-size:11px;color:#b8c9d6}.heat-cell{border:1px solid #325346;background:rgba(53,204,165,calc(.11 + .57*var(--heat)));color:#e6fff7;min-height:50px;border-radius:5px;font-size:12px;font-weight:700}.heat-cell:hover,.heat-cell.selected{border-color:#6ee1bd;box-shadow:inset 0 0 0 1px #71d9b6}.heat-key{display:flex;align-items:center;justify-content:flex-end;gap:10px;color:#8a9cab;font-size:10px;margin-top:15px}.heat-scale{height:8px;width:110px;border-radius:8px;background:linear-gradient(to right,#243a3d,#38cba2)}.portfolio-list{display:grid;gap:8px}.prod-row{display:grid;grid-template-columns:37px minmax(130px,1.3fr) minmax(70px,1fr) minmax(75px,.4fr);gap:14px;align-items:center;width:100%;border:1px solid #2b3a48;background:#14202c;border-radius:8px;padding:16px 13px;text-align:left;color:#dce8eb}.prod-row.on{background:#1c3a39;border-color:#5dba9f}.prod-number{color:#74b39f;font-size:13px}.prod-name{font-size:12px;font-weight:750}.prod-name small{font-size:10px;color:#88a3b1;display:block;margin-top:4px;font-weight:450}.prod-track{height:7px;background:#263b49;border-radius:9px;overflow:hidden}.prod-track i{display:block;height:100%;background:#44caac;border-radius:9px}.prod-row strong{font-size:12px;text-align:right}.field{display:grid;gap:9px;margin:0 0 19px;color:#a9b9c6;font-size:11px}.field>span{display:flex;justify-content:space-between;gap:10px}.field>span strong{color:#5ee2bf;font-size:12px}.field select{border:1px solid #35495a;background:#101f2b;border-radius:7px;padding:11px;color:#e7f3f5;width:100%}.rangefield input{width:100%;accent-color:#47cda9}.range-extents{display:flex;justify-content:space-between;color:#8295a7}.impact-big{background:#12302d;border:1px solid #28665c;padding:22px;border-radius:9px;display:grid;gap:8px;margin-bottom:20px}.impact-big span{color:#aaccc4;font-size:11px}.impact-big strong{color:#effdf9;font-size:31px}.impact-down,.neg{color:#f0a396!important}.good{color:#72e2b9!important}.duo{display:flex;gap:11px}.duo>div{flex:1;border:1px solid #304456;border-radius:7px;padding:12px}.duo span{color:#879aab;display:block;font-size:10px;margin-bottom:7px}.duo strong{font-size:17px}.comparebar{height:14px;position:relative;margin:20px 0}.comparebar i{height:13px;position:absolute;left:0;top:0;border-radius:3px}.spaced{margin-top:22px}.button{border:1px solid #45675e;border-radius:7px;padding:10px 15px;background:#19332d;color:#b4f7df;font-size:11px;font-weight:700}.button:hover{background:#285145}.button.primary{background:#39c6a6;color:#07251f}.actions{display:flex;gap:9px;flex-wrap:wrap;margin-top:15px}.upload-zone{border:1px dashed #526a7b;border-radius:10px;padding:29px;text-align:center;background:#11202a;display:flex;flex-direction:column;align-items:center;gap:9px}.upload-symbol{font-size:33px;color:#51d5b3}.upload-zone strong{font-size:15px}.upload-zone p{max-width:550px;color:#9aadb9;font-size:11px;margin:0;line-height:1.7}.file-label{display:inline-flex;margin:10px 0;cursor:pointer}.export-list{display:grid;gap:9px}.export-list button{padding:12px;border:1px solid #304254;background:#122434;border-radius:7px;color:#d7e9ee;text-align:left;display:grid;grid-template-columns:27px 1fr 17px;align-items:center;gap:10px;font-size:11px;font-weight:650}.export-list button:hover{border-color:#62bfa8}.export-icon{font-size:16px;color:#62d1b5}.table-note{font-size:10px;color:#8b9eae}.method-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}.method-grid strong{font-size:12px;color:#c9ede3}.method-grid p{font-size:11px;line-height:1.7;color:#98aabc}.disclaimer{border-color:#b68f58}.notice{border:1px solid #276956;background:#133b31;color:#a5f9da;padding:13px;border-radius:8px;font-size:12px;display:flex;align-items:center;justify-content:space-between}.notice.error{background:#3b2229;border-color:#9a535e;color:#ffd6d8}.notice button{color:inherit;border:0;background:transparent;font-size:19px}.footer{display:flex;justify-content:space-between;gap:15px;margin:28px 0 10px;font-size:9px;letter-spacing:1px;color:#607487}.mobile-nav{display:none}@media(max-width:1200px){.shell{grid-template-columns:220px minmax(0,1fr)}.main{padding:29px}.topbar{padding:0 27px}.sidebar{padding-left:9px;padding-right:9px}.metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.donut-layout{grid-template-columns:minmax(110px,175px) 1fr;gap:12px}}@media(max-width:850px){.shell{display:block}.sidebar{display:none}.topbar{height:59px;padding:0 18px}.breadcrumb{font-size:9px}.top-tag{font-size:9px}.topright{gap:9px}.main{padding:23px 16px 100px}.intro{display:block}.intro h1{font-size:28px}.dataset-control{margin-top:22px;max-width:420px}.two-col{grid-template-columns:1fr}.mobile-nav{display:grid;grid-template-columns:repeat(6,1fr);position:fixed;bottom:0;left:0;right:0;padding:10px 4px 12px;background:#0c1722;border-top:1px solid #314153;z-index:10}.mobile-nav button{border:0;background:none;color:#8599a9;display:grid;gap:2px;justify-items:center;font-size:18px}.mobile-nav small{font-size:9px}.mobile-nav button.active{color:#4cdbb4}.footer{display:block;line-height:2}}@media(max-width:510px){.metrics{gap:9px}.metric{padding:16px 13px;min-height:128px}.metric-top{font-size:10px}.metric>strong{font-size:21px}.metric p{font-size:9px}.panel{padding:16px}.donut-layout{grid-template-columns:150px minmax(0,1fr);gap:8px}.legend{gap:10px}.legend>div{gap:5px;font-size:10px}.detail-grid,.focus-grid{gap:9px}.detail-grid>div,.focus-grid>div{padding:12px}.detail-grid strong,.focus-grid strong{font-size:17px}.prod-row{grid-template-columns:20px minmax(115px,1fr) minmax(62px,.5fr);gap:7px;padding:13px 10px}.prod-track{display:none}.prod-row strong{font-size:11px}.method-grid{grid-template-columns:1fr}.eyebrow{font-size:8px}.intro p{font-size:11px}} | |
| @media(max-width:850px){.footer span{display:block;overflow-wrap:anywhere}} | |
| </style></head><body><div id="app"><div style="padding:60px;font-family:sans-serif">Loading TradeScope…</div></div><script> | |
| ; | |
| const E = (() => { | |
| const sum=a=>a.reduce((s,x)=>s+x,0); | |
| const round=(n,d=4)=>Number(n.toFixed(d)); | |
| const safe=(x)=>String(x??'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); | |
| const slug=s=>String(s).toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,''); | |
| function parseCSV(text){ | |
| const lines=[];let row=[],field='',inQ=false; | |
| text=String(text).replace(/^\uFEFF/,''); | |
| for(let i=0;i<text.length;i++){ | |
| const ch=text[i]; | |
| if(inQ){if(ch==='"'){if(text[i+1]==='"'){field+='"';i++;}else inQ=false;}else field+=ch;} | |
| else if(ch==='"'){if(field.trim()!=='')throw Error('Unexpected quote in CSV');inQ=true;} | |
| else if(ch===','){row.push(field);field='';} | |
| else if(ch==='\n'||ch==='\r'){if(ch==='\r'&&text[i+1]==='\n')i++;row.push(field);if(row.some(v=>v.trim()))lines.push(row);row=[];field='';} | |
| else field+=ch; | |
| } | |
| if(inQ)throw Error('CSV contains an unclosed quote'); | |
| row.push(field);if(row.some(v=>v.trim()))lines.push(row); | |
| if(!lines.length)throw Error('Empty CSV'); | |
| const cols=lines[0].map(c=>c.trim().toLowerCase()); | |
| if(new Set(cols).size!==cols.length)throw Error('Duplicate CSV column names'); | |
| if(lines.slice(1).some(r=>r.length!==cols.length))throw Error('Inconsistent number of CSV columns'); | |
| return lines.slice(1).map(r=>Object.fromEntries(cols.map((c,j)=>[c,r[j].trim()]))); | |
| } | |
| function validate(rows){ | |
| if(!Array.isArray(rows)||rows.length<4||rows.length>400)throw Error('CSV must contain 4 to 400 data rows'); | |
| const required=['product','market','exports_usd_m','tariff_pct','risk_score','growth_pct']; | |
| const keys=Object.keys(rows[0]||{});for(const k of required)if(!keys.includes(k))throw Error('Missing column: '+k); | |
| const pairs=new Set();const clean=rows.map((r,i)=>{ | |
| const product=String(r.product||'').trim(), market=String(r.market||'').trim(); | |
| if(!product||!market||product.length>70||market.length>70)throw Error('Invalid product/market in row '+(i+2)); | |
| if(/[<>]/.test(product+market))throw Error('HTML characters not allowed in names'); | |
| const key=product.toLowerCase()+'\0'+market.toLowerCase();if(pairs.has(key))throw Error('Repeated product-market pair in row '+(i+2));pairs.add(key); | |
| const vals=['exports_usd_m','tariff_pct','risk_score','growth_pct'].map(k=>{const raw=String(r[k]??'').trim();if(!raw||!/^[-+]?(?:\d+\.?\d*|\.\d+)$/.test(raw))throw Error('Invalid number for '+k+' in row '+(i+2));return Number(raw);}); | |
| const [v,t,risk,growth]=vals; | |
| if(!(v>0&&v<=1e6&&t>=0&&t<=100&&risk>=0&&risk<=100&&growth>=-100&&growth<=200))throw Error('Out-of-range value in row '+(i+2)); | |
| return {product,market,value:v,tariff:t,risk,growth}; | |
| }); | |
| if(new Set(clean.map(r=>r.market)).size<2)throw Error('At least two markets required'); | |
| if(new Set(clean.map(r=>r.product)).size<2)throw Error('At least two products required'); | |
| return clean; | |
| } | |
| function fromCSV(text){return validate(parseCSV(text));} | |
| function group(rows,by){const m=new Map();for(const r of rows){let g=m.get(r[by]);if(!g){g={name:r[by],value:0,weightedRisk:0,weightedTariff:0,weightedGrowth:0,count:0};m.set(r[by],g);}g.value+=r.value;g.weightedRisk+=r.value*r.risk;g.weightedTariff+=r.value*r.tariff;g.weightedGrowth+=r.value*r.growth;g.count++;}return [...m.values()].map(g=>({...g,risk:g.weightedRisk/g.value,tariff:g.weightedTariff/g.value,growth:g.weightedGrowth/g.value})).sort((a,b)=>b.value-a.value||a.name.localeCompare(b.name));} | |
| function analytics(rows){const total=sum(rows.map(r=>r.value)), markets=group(rows,'market'),products=group(rows,'product');const hhi=a=>sum(a.map(r=>(r.value/total)**2));const mhhi=hhi(markets), phhi=hhi(products);return {total, markets,products, marketHHI:mhhi,productHHI:phhi,marketEffective:1/mhhi,productEffective:1/phhi, marketEntropy:-sum(markets.map(g=>{let p=g.value/total;return p*Math.log(p)})),weightedTariff:sum(rows.map(r=>r.value*r.tariff))/total,weightedRisk:sum(rows.map(r=>r.value*r.risk))/total,weightedGrowth:sum(rows.map(r=>r.value*r.growth))/total};} | |
| function shock(rows,market,disruption,tariffPp,elasticity=1.5){ | |
| if(!rows.some(r=>r.market===market))throw Error('Unknown market'); | |
| const d=Math.max(0,Math.min(90,Number(disruption)))/100, t=Math.max(0,Math.min(40,Number(tariffPp)));if(!Number.isFinite(d+t))throw Error('Non-finite shock'); | |
| const factor=(1-d)*Math.max(0,1-elasticity*t/100); | |
| const altered=rows.map(r=>r.market===market?{...r,value:r.value*factor,tariff:Math.min(100,r.tariff+t)}:{...r}); | |
| const before=analytics(rows), after=analytics(altered);return {factor,before,after,loss:before.total-after.total,lossPct:(before.total-after.total)/before.total*100,altered}; | |
| } | |
| function diversify(rows,sourceMarket,desiredPct,maxNewPct=50){ | |
| if(!rows.some(r=>r.market===sourceMarket))throw Error('Unknown source market'); | |
| const pct=Math.max(0,Math.min(40,Number(desiredPct)))/100;if(!Number.isFinite(pct))throw Error('Invalid shift'); | |
| const altered=rows.map(r=>({...r}));let shifted=0,unallocated=0; | |
| const sources=altered.filter(r=>r.market===sourceMarket); | |
| for(const s of sources){let amount=s.value*pct;const targets=altered.filter(r=>r.product===s.product &&r.market!==sourceMarket &&r.risk<s.risk &&r.tariff<=s.tariff+15).map(r=>({r,capacity:r.value*(maxNewPct/100),score:Math.max(0.1,110-r.risk)*Math.max(0.1,1+r.growth/100)/(1+r.tariff/100)})).sort((a,b)=>b.score-a.score); | |
| const requested=amount; | |
| // Deterministic greedy allocation by growth-adjusted risk/tariff score; not globally optimal. | |
| for(const t of targets){const give=Math.min(amount,t.capacity);t.r.value+=give;s.value-=give;shifted+=give;amount-=give;if(amount<1e-10)break;} | |
| unallocated+=requested- (requested-amount); | |
| } | |
| const before=analytics(rows), after=analytics(altered);return {before,after,altered,shifted,requested:sum(sources.map(s=>rows.find(r=>r.product===s.product&&r.market===sourceMarket).value*pct)),unallocated:sum(sources.map(s=>rows.find(r=>r.product===s.product&&r.market===sourceMarket).value*pct))-shifted}; | |
| } | |
| function csvEscape(v){let s=String(v??'');if(typeof v!=='number' && /^[=+@\-\t\r]/.test(s))s="'"+s;return '"'+s.replace(/"/g,'""')+'"';} | |
| function toCSV(rows){return [['product','market','exports_usd_m','tariff_pct','risk_score','growth_pct'],...rows.map(r=>[r.product,r.market,r.value,r.tariff,r.risk,r.growth])].map(r=>r.map(csvEscape).join(',')).join('\r\n');} | |
| function demos(){ | |
| const sets=[{key:'andes',title:'Andean Export Portfolio',subtitle:'Agri-food exporters · synthetic',products:['Quinoa','Specialty Coffee','Olive Oil','Dried Fruit','Natural Fibres'],markets:['Brazil','Chile','Germany','United States','Japan'],bases:[45,30,26,35,14],factor:[1.15,1.05,.8,1.35,.65],risk:[27,18,22,38,17],tariff:[6,3,9,11,5],growth:[7,4,2,9,6]}, | |
| {key:'energy',title:'Clean Energy Components',subtitle:'Equipment supply chains · synthetic',products:['Solar Modules','Battery Packs','Smart Sensors','Inverters','Grid Controls'],markets:['Mexico','Spain','India','South Korea','Canada'],bases:[48,42,19,28,35],factor:[1.3,.9,1.4,.85,.75],risk:[36,18,48,25,12],tariff:[12,5,16,8,4],growth:[9,5,13,7,6]}, | |
| {key:'services',title:'Digital Services Exports',subtitle:'B2B software markets · synthetic',products:['Cloud Support','Cyber Audits','UX Services','Data Analytics','Remote Training'],markets:['Argentina','Colombia','United States','United Kingdom','Australia'],bases:[26,31,19,36,21],factor:[.85,1,1.6,1.05,.7],risk:[42,34,20,17,14],tariff:[3,4,1,2,1],growth:[6,8,11,5,7]}]; | |
| return sets.map(s=>({key:s.key,title:s.title,subtitle:s.subtitle,rows:s.products.flatMap((p,i)=>s.markets.map((m,j)=>({product:p,market:m,value:round(s.bases[i]*s.factor[j]*(.66+((i*5+j*3)%7)*.078),2),risk:Math.min(100,s.risk[j]+i*2),tariff:Math.min(100,s.tariff[j]+i%3),growth:s.growth[j]+i-2})))})); | |
| } | |
| return {sum,round,safe,slug,parseCSV,validate,fromCSV,group,analytics,shock,diversify,csvEscape,toCSV,demos}; | |
| })(); | |
| if (typeof module !== 'undefined' && module.exports) module.exports=E; | |
| ; | |
| const DEMOS=E.demos(); | |
| const state={dataset:DEMOS[0],active:'overview',market:'Brazil',product:'Quinoa',disruption:25,tariffPp:8,shiftPct:20,view:'market',notice:null}; | |
| const ids=['overview','matrix','products','stress','planner','data']; | |
| const labels={overview:'Intelligence Overview',matrix:'Market Matrix',products:'Product Portfolio',stress:'Trade Shock Studio',planner:'Diversification Planner',data:'Data & Methodology'}; | |
| const icons={overview:'◈',matrix:'▦',products:'▤',stress:'⚡',planner:'◎',data:'◫'}; | |
| const $=sel=>document.querySelector(sel); | |
| const fmt=(n,d=1)=>new Intl.NumberFormat('en-US',{maximumFractionDigits:d,minimumFractionDigits:d}).format(n); | |
| const money=n=>'$'+fmt(n,1)+'M'; | |
| const pct=n=>fmt(n,1)+'%'; | |
| const esc=E.safe; | |
| const button=(label,action,extra='')=>`<button class="button ${extra}" data-action="${action}">${label}</button>`; | |
| const panel=(title,desc,body,extra='')=>`<section class="panel ${extra}"><div class="panel-head"><div><h3>${title}</h3><p>${desc}</p></div></div>${body}</section>`; | |
| const tag=(a,t='neutral')=>`<span class="tag ${t}">${a}</span>`; | |
| const bars=(items,max,key='value',end='')=>`<div class="bar-stack">${items.map((r,i)=>`<div class="bar-item"><div class="bar-meta"><strong>${esc(r.name)}</strong><span>${end==='percent'?pct(r[key]):money(r[key])}</span></div><div class="bar-track"><div class="bar-fill color-${i%5}" style="width:${Math.max(0.7,100*r[key]/(max||1)).toFixed(2)}%"></div></div></div>`).join('')}</div>`; | |
| const duo=(a,b)=>`<div class="duo"><div><span>Baseline</span><strong>${money(a)}</strong></div><div><span>Scenario</span><strong>${money(b)}</strong></div></div>`; | |
| const rowTable=(cols, rows)=>`<div class="tablewrap"><table><thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead><tbody>${rows.join('')}</tbody></table></div>`; | |
| function donut(data,total){let acc=0;const palette=['#37c9a9','#7298ff','#deb875','#ef8e7f','#a49afa','#8094aa'];let rings='';data.forEach((x,i)=>{let len=x.value/total*100;let off=-acc; rings+=`<circle cx="100" cy="100" r="72" fill="none" stroke="${palette[i%palette.length]}" stroke-width="21" stroke-dasharray="${len.toFixed(4)} ${100-len}" stroke-dashoffset="${off.toFixed(4)}" transform="rotate(-90 100 100)" pathLength="100"/>`;acc+=len;});return `<div class="donut-layout"><svg viewBox="0 0 200 200" aria-label="Market shares donut chart" role="img"><circle cx="100" cy="100" r="72" stroke="#263141" fill="none" stroke-width="21"/>${rings}<text x="100" y="96" text-anchor="middle" class="chart-big">${data.length}</text><text x="100" y="116" text-anchor="middle" class="chart-small">markets</text></svg><div class="legend">${data.map((x,i)=>`<div><i style="background:${palette[i%palette.length]}"></i><span>${esc(x.name)}</span><strong>${pct(x.value/total*100)}</strong></div>`).join('')}</div></div>`;} | |
| function lineChart(rows){const max=Math.max(...rows.map(r=>r.value));return `<div class="chart-header"><span>Market export revenue</span><span class="quiet">USD millions · synthetic</span></div>${bars(rows,max)}`;} | |
| function navItem(id){return `<button class="nav-item ${state.active===id?'active':''}" data-nav="${id}"><span class="nav-icon">${icons[id]}</span><span>${labels[id]}</span>${state.active===id?'<i>●</i>':''}</button>`;} | |
| function metrics(a){const list=[['Export portfolio',money(a.total),'Sum of observed trade flows'],['Market concentration',fmt(a.marketHHI*10000,0),'HHI on a 0–10,000 scale'],['Effective markets',fmt(a.marketEffective,2),'Inverse market HHI'],['Weighted risk',fmt(a.weightedRisk,1)+'/100','Export-weighted risk indicator']];return `<div class="metrics">${list.map((r,i)=>`<div class="metric"><div class="metric-top"><span>${r[0]}</span><span class="metric-icon">${['◈','▥','◎','◇'][i]}</span></div><strong>${r[1]}</strong><p>${r[2]}</p></div>`).join('')}</div>`;} | |
| function overview(a,rows){return `${metrics(a)}<div class="two-col">${panel('Market composition','Export shares by destination; synthetic demonstration',donut(a.markets,a.total))}${panel('Destination ranking','Markets ordered by total export value',lineChart(a.markets))}</div><div class="two-col">${panel('Exposure diagnostics','Portfolio-level weighted assumptions',`<div class="detail-grid"><div><span>Weighted tariff</span><strong>${pct(a.weightedTariff)}</strong></div><div><span>Weighted growth</span><strong>${pct(a.weightedGrowth)}</strong></div><div><span>Product HHI</span><strong>${fmt(a.productHHI*10000,0)}</strong></div><div><span>Trade observations</span><strong>${rows.length}</strong></div></div><div class="help">HHI measures concentration. High export value in one market increases exposure to market-specific disruptions.</div>`)}${panel('Market risk dashboard','Assumed risk and tariff parameters, not external live feeds',rowTable(['Market','Risk / 100','Tariff','Share'],a.markets.map(r=>`<tr><td>${esc(r.name)}</td><td>${riskLabel(r.risk)}</td><td>${pct(r.tariff)}</td><td>${pct(r.value/a.total*100)}</td></tr>`)))}</div>`;} | |
| const riskLabel=r=>`<span class="risk ${r>=50?'high':r>=30?'medium':'low'}">${fmt(r,0)}</span>`; | |
| function matrix(a,rows){const prods=[...new Set(rows.map(r=>r.product))],markets=[...new Set(rows.map(r=>r.market))],peak=Math.max(...rows.map(r=>r.value));let matrix=`<div class="matrix-wrap"><div class="heatmap" style="--cols:${markets.length}"><div class="heat-head">Product / market</div>${markets.map(m=>`<div class="heat-market">${esc(m)}</div>`).join('')}${prods.map(p=>`<div class="heat-product">${esc(p)}</div>${markets.map(m=>{const r=rows.find(r=>r.product===p&&r.market===m);const q=r?Math.pow(r.value/peak,.7):0;return `<button class="heat-cell ${state.market===m?'selected':''}" data-market="${esc(m)}" style="--heat:${q.toFixed(3)}" title="${esc(p)} to ${esc(m)}: ${money(r?.value||0)}">${r?fmt(r.value,1):'–'}</button>`;}).join('')}`).join('')}</div></div><div class="heat-key">Low volume <div class="heat-scale"></div> High volume</div>`; | |
| const focus=a.markets.find(m=>m.name===state.market)||a.markets[0];return `${panel('Product × destination trade matrix','Click any cell to inspect that destination; figures in USD millions',matrix)}<div class="two-col">${panel('Selected destination',`Focused view: ${esc(focus.name)}`,`<div class="focus-grid"><div><span>Total export value</span><strong>${money(focus.value)}</strong></div><div><span>Market share</span><strong>${pct(focus.value/a.total*100)}</strong></div><div><span>Risk index</span><strong>${fmt(focus.risk,1)}/100</strong></div><div><span>Mean tariff</span><strong>${pct(focus.tariff)}</strong></div></div>`)}${panel('Top exposures',`Export products shipped to ${esc(focus.name)}`,bars(rows.filter(r=>r.market===focus.name).map(r=>({name:r.product,value:r.value})).sort((x,y)=>y.value-x.value),Math.max(...rows.filter(r=>r.market===focus.name).map(r=>r.value))))}</div>`;} | |
| function products(a,rows){let all=a.products;const focus=all.find(p=>p.name===state.product)||all[0];const ms=rows.filter(r=>r.product===focus.name).sort((a,b)=>b.value-a.value);return `${panel('Product concentration','Export portfolio by product',`<div class="portfolio-list">${all.map((p,i)=>`<button class="prod-row ${p.name===focus.name?'on':''}" data-product="${esc(p.name)}"><span class="prod-number">0${i+1}</span><span class="prod-name">${esc(p.name)}<small>${pct(p.value/a.total*100)} of portfolio</small></span><span class="prod-track"><i style="width:${pct(p.value/all[0].value*100)}"></i></span><strong>${money(p.value)}</strong></button>`).join('')}</div>`)}<div class="two-col">${panel('Selected product',esc(focus.name),`<div class="focus-grid"><div><span>Total exports</span><strong>${money(focus.value)}</strong></div><div><span>Market reach</span><strong>${ms.length}</strong></div><div><span>Weighted growth</span><strong>${pct(focus.growth)}</strong></div><div><span>Weighted risk</span><strong>${fmt(focus.risk,1)}</strong></div></div>`)}${panel('Destination breakdown','Revenue generated in each destination',bars(ms.map(r=>({name:r.market,value:r.value})),ms[0].value))}</div>`;} | |
| function selectMarket(){return `<label class="field"><span>Target destination</span><select id="market-select">${E.group(state.dataset.rows,'market').map(m=>`<option value="${esc(m.name)}" ${m.name===state.market?'selected':''}>${esc(m.name)}</option>`).join('')}</select></label>`;} | |
| function slider(id,name,val,min,max,step=1,suffix='%'){return `<label class="field rangefield"><span>${name}<strong>${val}${suffix}</strong></span><input id="${id}" type="range" min="${min}" max="${max}" step="${step}" value="${val}"><div class="range-extents"><small>${min}${suffix}</small><small>${max}${suffix}</small></div></label>`;} | |
| function stress(a,rows){const s=E.shock(rows,state.market,state.disruption,state.tariffPp);return `<div class="two-col config-col">${panel('Shock assumptions','Adjust synthetic market disruption and incremental tariff',`${selectMarket()}${slider('disruption','Demand disruption',state.disruption,0,80,5)}${slider('tariff','Additional tariff',state.tariffPp,0,30,1,' pp')}<div class="help">Affected market export value is multiplied by (1 − disruption) × max(0, 1 − 1.5 × tariff increase / 100). This is a stylized elasticity assumption, not a demand forecast.</div>`)}${panel('Portfolio impact','Before and after the combined shock',`<div class="impact-big"><span>Estimated export value at risk</span><strong>${money(s.loss)}</strong><span class="impact-down">−${pct(s.lossPct)} of baseline trade value</span></div>${duo(s.before.total,s.after.total)}<div class="comparebar"><i style="width:100%;background:#38465b"></i><i style="width:${100*s.after.total/s.before.total}%;background:#3acaa9"></i></div><div class="help">Only exports in the selected destination are affected. Other markets remain unchanged in this simplified model.</div>`)}</div><div class="two-col">${panel('Market-level outcomes','Export revenue before and after disruption',rowTable(['Destination','Baseline','Stress','Difference'],a.markets.map(m=>{const after=s.after.markets.find(z=>z.name===m.name);return `<tr><td>${esc(m.name)}</td><td>${money(m.value)}</td><td>${money(after?.value||0)}</td><td class="${m.name===state.market?'neg':''}">${m.name===state.market?'−'+money(m.value-(after?.value||0)):'—'}</td></tr>`;})))}${panel('Concentration shift','Recomputed portfolio exposure',`<div class="focus-grid"><div><span>Baseline market HHI</span><strong>${fmt(a.marketHHI*10000,0)}</strong></div><div><span>After-shock market HHI</span><strong>${fmt(s.after.marketHHI*10000,0)}</strong></div><div><span>Baseline risk index</span><strong>${fmt(a.weightedRisk,1)}</strong></div><div><span>After-shock risk index</span><strong>${fmt(s.after.weightedRisk,1)}</strong></div></div><p class="help">Shares and risk scores are recomputed from the adjusted simulated export values.</p>`)}</div>`;} | |
| function planner(a,rows){const p=E.diversify(rows,state.market,state.shiftPct);return `<div class="two-col config-col">${panel('Diversification assumptions','Shift part of this market’s product exports to existing buyers',`${selectMarket()}${slider('shift','Target share to reallocate',state.shiftPct,0,40,5)}<div class="help">For each product, only destinations with a lower assumed risk and no more than 15 percentage points higher tariff qualify. Each destination can absorb at most 50% of its existing product volume. The deterministic greedy allocation favors lower risk and faster growth; it does not claim a global optimum.</div>`)}${panel('Potential reallocation','What the model can redistribute',`<div class="impact-big"><span>Feasible reallocation</span><strong>${money(p.shifted)}</strong><span class="${p.shifted>0?'good':'quiet'}">${pct(100*p.shifted/a.total)} of portfolio</span></div>${duo(p.before.total,p.after.total)}<div class="focus-grid spaced"><div><span>Requested shift</span><strong>${money(p.requested)}</strong></div><div><span>Unallocated</span><strong>${money(p.unallocated)}</strong></div><div><span>Risk score: baseline</span><strong>${fmt(p.before.weightedRisk,1)}</strong></div><div><span>Risk score: proposed</span><strong>${fmt(p.after.weightedRisk,1)}</strong></div></div>`)}</div><div class="two-col">${panel('Proposed destination allocations','Reallocation does not create additional total export value',rowTable(['Destination','Current','Proposed','Change'],a.markets.map(m=>{const n=p.after.markets.find(r=>r.name===m.name);const delta=n.value-m.value;return `<tr><td>${esc(m.name)}</td><td>${money(m.value)}</td><td>${money(n.value)}</td><td class="${delta>0?'good':delta<0?'neg':''}">${delta>0?'+':''}${money(delta)}</td></tr>`;})))}${panel('Decision-quality metrics','Compare concentration and exposure before and after reallocation',`<div class="focus-grid"><div><span>Market HHI (current)</span><strong>${fmt(p.before.marketHHI*10000,0)}</strong></div><div><span>Market HHI (proposed)</span><strong>${fmt(p.after.marketHHI*10000,0)}</strong></div><div><span>Tariff (current)</span><strong>${pct(p.before.weightedTariff)}</strong></div><div><span>Tariff (proposed)</span><strong>${pct(p.after.weightedTariff)}</strong></div></div><div class="help">These are hypothetical shifts. Trade agreements, sales capacity, regulations, transportation and customer acquisition costs are not modeled.</div>`)}</div>`;} | |
| function dataMethods(a,rows){return `${panel('Import trade-flow data','Local CSV only. Your uploaded files never leave this browser.',`<div class="upload-zone"><span class="upload-symbol">⇧</span><strong>Import your own export flows</strong><p>CSV: product, market, exports_usd_m, tariff_pct, risk_score, growth_pct</p><label class="button primary file-label">Select CSV<input id="file-import" type="file" accept=".csv,text/csv" hidden></label><p class="quiet">4–400 rows · 2+ products · 2+ destinations · one row per product-market pair</p></div><div class="actions">${button('Download CSV template','template')}${button('Restore demo dataset','reset')}</div>`)}<div class="two-col">${panel('Data preview','Current active trade-flow observations',rowTable(['Product','Destination','Exports (USD M)','Risk'],rows.slice(0,12).map(r=>`<tr><td>${esc(r.product)}</td><td>${esc(r.market)}</td><td>${money(r.value)}</td><td>${fmt(r.risk,0)}</td></tr>`))+`<p class="table-note">Displaying 12 of ${rows.length} rows</p>`)}${panel('Reports & exports','All reports describe the selected dataset and current scenario assumptions',`<div class="export-list">${[['Download portfolio CSV','dataset'],['Download JSON audit','json'],['Download Markdown report','markdown'],['Download stress scenario CSV','stress-csv'],['Download diversification CSV','diversification-csv']].map((x,i)=>`<button data-action="${x[1]}"><span class="export-icon">${['▦','{}','≡','↯','◎'][i]}</span><span>${x[0]}</span><span>↓</span></button>`).join('')}</div>`)}</div>${panel('Methods, validation and limitations','Transparent formulas and responsible interpretation',`<div class="method-grid"><div><strong>Market HHI</strong><p>Σ(market exports / all exports)². Displayed on 0–10,000 scale. Effective markets = 1 / HHI.</p></div><div><strong>Weighted exposure</strong><p>Weighted average risk, tariff and growth assumptions based on each observed export flow.</p></div><div><strong>Shock simulator</strong><p>Market-specific volume loss and incremental-tariff response use a stylized elasticity factor of 1.5.</p></div><div><strong>Diversification</strong><p>Greedy lower-risk redistribution with per-destination volume caps. Not an exact global optimization.</p></div></div><p class="disclaimer">All bundled examples are synthetic and no actual export statistics, official risk scores or up-to-date tariff schedules are supplied. Import data from verified sources before relying on results. The application is educational and does not provide investment, financial or legal advice.</p>`)}`;} | |
| function content(a,rows){switch(state.active){case 'overview':return overview(a,rows);case 'matrix':return matrix(a,rows);case 'products':return products(a,rows);case 'stress':return stress(a,rows);case 'planner':return planner(a,rows);case 'data':return dataMethods(a,rows);}} | |
| function render(){const rows=state.dataset.rows,a=E.analytics(rows);const markets=a.markets.map(m=>m.name),products=a.products.map(p=>p.name);if(!markets.includes(state.market))state.market=markets[0];if(!products.includes(state.product))state.product=products[0]; | |
| $('#app').innerHTML=`<div class="shell"><aside class="sidebar"><a class="brand" href="#" data-nav="overview"><div class="brand-icon">T<span>·</span></div><div><strong>TradeScope<span class="period">.</span></strong><small>GLOBAL COMMERCE INTELLIGENCE</small></div></a><div class="side-label">WORKSPACE</div><div class="menu">${ids.map(navItem).join('')}</div><div class="sidebar-bottom"><div class="status"><i></i> Offline analytical engine</div><div class="side-small">PROJECT 47 · RESEARCH LAB<br>© 2026 TradeScope · MIT</div></div></aside><div class="workspace"><header class="topbar"><div class="breadcrumb">RESEARCH WORKSPACE <span>/</span> ${labels[state.active].toUpperCase()}</div><div class="topright"><span class="top-tag">● LOCAL-FIRST</span><span class="top-avatar">TS</span></div></header><main class="main"><div class="intro"><div><div class="eyebrow"><span class="eyebrow-line"></span> GLOBAL COMMERCE ANALYTICS <span class="eyebrow-number">// 047</span></div><h1>${labels[state.active]}</h1><p>${state.active==='overview'?'Measure market concentration, export exposure and commercial resilience from transparent, reproducible inputs.':{matrix:'Explore product-market relationships, revenue distribution and destination exposure.',products:'Track the mix of exports and where each product generates value.',stress:'Test stylized market disruptions and tariff changes against your portfolio.',planner:'Model feasible redistribution of existing exports toward less exposed destinations.',data:'Inspect assumptions, import your own CSV files and export an audit trail.'}[state.active]}</p></div><div class="dataset-control"><label>ACTIVE DATASET</label><select id="dataset-select">${DEMOS.map(d=>`<option value="${d.key}" ${state.dataset.key===d.key?'selected':''}>${esc(d.title)}</option>`).join('')}${state.dataset.key==='custom'?`<option value="custom" selected>Imported CSV dataset</option>`:''}</select><small>${esc(state.dataset.subtitle)}</small></div></div>${state.notice?`<div class="notice ${state.notice.type}"><span>${state.notice.type==='error'?'⚠':'✓'} ${esc(state.notice.text)}</span><button data-action="dismiss" aria-label="Dismiss message">×</button></div>`:''}<div class="content">${content(a,rows)}</div><footer class="footer"><span>TRADESCOPE · SYNTHETIC DATA FOR DEMONSTRATION</span><span>Private by design · Browser-only execution</span></footer></main></div></div><div class="mobile-nav">${ids.map(id=>`<button data-nav="${id}" class="${state.active===id?'active':''}" title="${labels[id]}"><span>${icons[id]}</span><small>${['Home','Matrix','Mix','Stress','Plan','Data'][ids.indexOf(id)]}</small></button>`).join('')}</div>`; | |
| } | |
| function report(){const a=E.analytics(state.dataset.rows);const stress=E.shock(state.dataset.rows,state.market,state.disruption,state.tariffPp);const plan=E.diversify(state.dataset.rows,state.market,state.shiftPct);return {application:'TradeScope Project 47',version:'1.0.0',dataset:state.dataset.title,synthetic:state.dataset.key!=='custom',assumptions:{market:state.market,disruption_pct:state.disruption,tariff_increase_pp:state.tariffPp,elasticity:1.5,diversification_request_pct:state.shiftPct,max_destination_volume_increase_pct:50},summary:{total_exports_usd_m:E.round(a.total),market_hhi_10000:E.round(a.marketHHI*10000),effective_markets:E.round(a.marketEffective),weighted_risk:E.round(a.weightedRisk),weighted_tariff_pct:E.round(a.weightedTariff),markets:a.markets,products:a.products},stress:{loss_usd_m:E.round(stress.loss),loss_pct:E.round(stress.lossPct)},diversification:{requested_usd_m:E.round(plan.requested),feasible_usd_m:E.round(plan.shifted),unallocated_usd_m:E.round(plan.unallocated),before_risk:E.round(a.weightedRisk),after_risk:E.round(plan.after.weightedRisk)},methodology:'Deterministic export-share HHI, linear stylized tariff response, and rule-based diversification. Not real-time data, not predictive.',data:state.dataset.rows};} | |
| function download(content,name,type){const blob=new Blob([content],{type});const u=URL.createObjectURL(blob);const a=document.createElement('a');a.href=u;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(u),10000);} | |
| function downloadAction(action){const rows=state.dataset.rows;const stem='tradescope-'+E.slug(state.dataset.key);const csv=arr=>E.toCSV(arr.map(r=>({...r,product:r.product,market:r.market}))); | |
| if(action==='template'){download('product,market,exports_usd_m,tariff_pct,risk_score,growth_pct\r\n"Quinoa","Chile",25,5,20,6\r\n"Quinoa","Germany",15,9,24,4\r\n"Olive Oil","Chile",12,6,20,5\r\n"Olive Oil","Germany",8,8,24,3', 'tradescope-template.csv','text/csv');return;} | |
| if(action==='dataset'){download(E.toCSV(rows),stem+'-portfolio.csv','text/csv');return;} | |
| if(action==='json'){download(JSON.stringify(report(),null,2),stem+'-audit.json','application/json');return;} | |
| if(action==='markdown'){const r=report();const str=`# TradeScope analytical report\n\nDataset: ${r.dataset}\nSynthetic bundled data: ${r.synthetic?'Yes':'No, user imported'}\n\n## Portfolio\n- Total trade: ${money(r.summary.total_exports_usd_m)}\n- Market HHI: ${fmt(r.summary.market_hhi_10000,0)}\n- Effective markets: ${fmt(r.summary.effective_markets,2)}\n- Weighted risk: ${fmt(r.summary.weighted_risk,1)}/100\n\n## Stress scenario\n- Destination: ${r.assumptions.market}\n- Demand disruption: ${r.assumptions.disruption_pct}%\n- Additional tariff: ${r.assumptions.tariff_increase_pp} pp\n- Modeled loss: ${money(r.stress.loss_usd_m)} (${pct(r.stress.loss_pct)})\n\n## Diversification\n- Requested reallocation: ${money(r.diversification.requested_usd_m)}\n- Feasible reallocation: ${money(r.diversification.feasible_usd_m)}\n- Unallocated request: ${money(r.diversification.unallocated_usd_m)}\n\n## Limitations\n- ${r.methodology}\n`;download(str,stem+'-report.md','text/markdown');return;} | |
| if(action==='stress-csv'){download(csv(E.shock(rows,state.market,state.disruption,state.tariffPp).altered),stem+'-stress.csv','text/csv');return;} | |
| if(action==='diversification-csv'){download(csv(E.diversify(rows,state.market,state.shiftPct).altered),stem+'-reallocation.csv','text/csv');return;} | |
| } | |
| function notice(text,type='success'){state.notice={type,text};render();} | |
| $('#app').addEventListener('click',e=>{const nav=e.target.closest('[data-nav]');if(nav){e.preventDefault();state.active=nav.dataset.nav;state.notice=null;render();window.scrollTo(0,0);return;}const mark=e.target.closest('[data-market]');if(mark){state.market=mark.dataset.market;render();return;}const prod=e.target.closest('[data-product]');if(prod){state.product=prod.dataset.product;render();return;}const b=e.target.closest('[data-action]');if(!b)return;const action=b.dataset.action;if(action==='dismiss'){state.notice=null;render();return;}if(action==='reset'){state.dataset=DEMOS[0];state.market=state.dataset.rows[0].market;state.product=state.dataset.rows[0].product;notice('Demo dataset restored.');return;}try{downloadAction(action);}catch(err){notice(err.message,'error');}}); | |
| $('#app').addEventListener('change',e=>{const id=e.target.id;const val=e.target.value;if(id==='dataset-select'){const next=DEMOS.find(d=>d.key===val);if(next){state.dataset=next;state.market=E.group(next.rows,'market')[0].name;state.product=E.group(next.rows,'product')[0].name;state.notice=null;render();}}else if(id==='market-select'){state.market=val;render();}else if(id==='disruption'){state.disruption=Number(val);render();}else if(id==='tariff'){state.tariffPp=Number(val);render();}else if(id==='shift'){state.shiftPct=Number(val);render();}else if(id==='file-import'){const f=e.target.files?.[0];if(!f)return;if(f.size>350000){notice('CSV file is too large (maximum 350 KB).','error');return;}const reader=new FileReader();reader.onerror=()=>notice('Unable to read CSV.','error');reader.onload=()=>{try{const parsed=E.fromCSV(reader.result);state.dataset={key:'custom',title:'Imported CSV dataset',subtitle:'User-provided local data',rows:parsed};state.market=E.group(parsed,'market')[0].name;state.product=E.group(parsed,'product')[0].name;notice(`Successfully imported ${parsed.length} trade flows.`);}catch(err){notice(err.message,'error');}};reader.readAsText(f,'utf-8');}}); | |
| render(); | |
| </script></body></html> | |