Polish return charts

This commit is contained in:
Theodore Song
2026-07-12 11:41:10 -04:00
parent 59eb2f61a7
commit 5e09bdbdcc
+51 -24
View File
@@ -107,10 +107,11 @@ h1,h2,h3,.brand-name{font-family:'Space Grotesk','Inter',sans-serif}
.pos-val{color:var(--green)}.neg-val{color:var(--red)}
/* ---------- charts ---------- */
#chart,#chart2{width:100%;height:240px;display:block}
.legend{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:14px}
.leg{display:flex;align-items:center;gap:7px;font-size:12.5px;color:#cdd6e8}
.legdot{width:10px;height:10px;border-radius:3px;display:inline-block}
#chart,#chart2{width:100%;height:300px;display:block;margin-top:6px;overflow:visible}
.legend{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:8px 12px;margin-top:14px}
.leg{display:flex;align-items:center;gap:8px;font-size:12px;color:#cdd6e8;background:rgba(255,255,255,.035);border:1px solid rgba(255,255,255,.06);border-radius:8px;padding:7px 9px;min-width:0}
.leg b{margin-left:auto;font-family:'Space Grotesk',sans-serif}
.legdot{width:9px;height:9px;border-radius:50%;display:inline-block;box-shadow:0 0 0 3px rgba(255,255,255,.06)}
.rangebar{display:flex;gap:5px;flex-wrap:wrap;justify-content:flex-end}
.rangebtn{appearance:none;border:1px solid var(--border);background:rgba(255,255,255,.04);color:var(--muted);
font:700 11px 'Inter',sans-serif;padding:5px 8px;border-radius:8px;cursor:pointer}
@@ -223,6 +224,8 @@ a.market-title:hover{color:#fff;text-decoration:underline;text-decoration-color:
.btn{padding:9px 11px}
.section-title{font-size:19px}
.stats{grid-template-columns:1fr;gap:12px}
#chart,#chart2{height:280px}
.legend{grid-template-columns:1fr}
.agent-brief{grid-template-columns:1fr}
.pos{align-items:flex-start;gap:12px}
.pos .pq{max-width:none}
@@ -311,7 +314,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<div class="grid2">
<div class="card hoverable">
<div class="card-h"><h3>Agent Returns</h3><div><span class="small muted" id="lastRun"></span><div class="rangebar" data-chart-ranges></div></div></div>
<svg id="chart" viewBox="0 0 600 240" preserveAspectRatio="none"></svg>
<svg id="chart" viewBox="0 0 720 300" preserveAspectRatio="none"></svg>
<div class="legend" id="comboLegend"></div>
<div class="small muted" style="margin-top:10px">Use the range buttons to zoom from one week to all history. New live points are added whenever you click "Run cycle"; the dashed line tracks the average agent return.</div>
</div>
@@ -328,7 +331,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<div class="lb-grid" id="leaderboard"></div>
<div class="card">
<div class="card-h"><h3>Returns — all agents</h3><div><span class="small muted">$10,000 start each</span><div class="rangebar" data-chart-ranges></div></div></div>
<svg id="chart2" viewBox="0 0 600 240" preserveAspectRatio="none"></svg>
<svg id="chart2" viewBox="0 0 720 300" preserveAspectRatio="none"></svg>
<div class="legend" id="comboLegend2"></div>
<div class="small muted" style="margin-top:10px">Past week is a backtest. After that, each manual or daily run adds another return snapshot for every agent.</div>
</div>
@@ -1262,6 +1265,25 @@ function chartTicks(times,maxTicks=5){
}
return ticks;
}
function niceChartBounds(vals){
let min=Math.min(...vals,0),max=Math.max(...vals,0);
if(min===max){min-=1;max+=1;}
const span=max-min,pad=Math.max(0.5,span*0.12);
min-=pad;max+=pad;
const step=Math.max(0.5,Math.pow(10,Math.floor(Math.log10(max-min)))/2);
min=Math.floor(min/step)*step;max=Math.ceil(max/step)*step;
return {min,max};
}
function smoothPath(points){
if(points.length<2)return points.map((p,i)=>`${i?"L":"M"}${p.x},${p.y}`).join(" ");
let d=`M${points[0].x},${points[0].y}`;
for(let i=1;i<points.length;i++){
const prev=points[i-1],cur=points[i];
const midX=((+prev.x)+(+cur.x))/2;
d+=` C${midX.toFixed(1)},${prev.y} ${midX.toFixed(1)},${cur.y} ${cur.x},${cur.y}`;
}
return d;
}
function rangeFilteredTimes(times){
const range=CHART_RANGES.find(r=>r.id===chartRange())||CHART_RANGES[0];
if(!range.days)return times;
@@ -1278,7 +1300,7 @@ function drawCombo(svgId,legendId){
const allTimes=rangeFilteredTimes([...new Set(series.flatMap(s=>s.snaps.map(snapTime)))].sort((a,b)=>a-b));
const svg=$(svgId); if(!svg)return;
if(!allTimes.length){svg.innerHTML="";if($(legendId))$(legendId).innerHTML=`<span class="muted small">No history yet — click "Run cycle".</span>`;return;}
const W=600,H=240,padL=42,padR=18,padT=18,padB=38; const vals=[0];
const W=720,H=300,padL=58,padR=28,padT=34,padB=50; const vals=[0];
const lines=series.map(s=>{
let last=0,idx=0;
const pts=allTimes.map(t=>{
@@ -1292,37 +1314,42 @@ function drawCombo(svgId,legendId){
});
const avgPts=allTimes.map((_,i)=>+(lines.reduce((sum,l)=>sum+l.pts[i],0)/Math.max(1,lines.length)).toFixed(2));
vals.push(...avgPts);
const min=Math.min(...vals,0),max=Math.max(...vals,0),span=(max-min)||1;
const bounds=niceChartBounds(vals),min=bounds.min,max=bounds.max,span=(max-min)||1;
const x=i=>allTimes.length===1?(padL+(W-padR))/2:padL+(i/(allTimes.length-1))*(W-padL-padR);
const y=v=>H-padB-((v-min)/span)*(H-padT-padB);
const yTicks=[...new Set([min,(min+max)/2,max].map(v=>+v.toFixed(1)))];
const yTicks=Array.from({length:5},(_,i)=>+(min+(span*i/4)).toFixed(1));
const zeroY=y(0).toFixed(1);
let inner=`<defs>
<linearGradient id="${svgId}-fade" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="rgba(124,140,255,.14)"/><stop offset="100%" stop-color="rgba(124,140,255,0)"/>
<linearGradient id="${svgId}-panel" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="rgba(255,255,255,.055)"/><stop offset="100%" stop-color="rgba(255,255,255,.018)"/>
</linearGradient>
<filter id="${svgId}-soft" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="4" stdDeviation="5" flood-color="rgba(0,0,0,.22)"/>
</filter>
</defs>`;
inner+=`<rect x="${padL}" y="${padT}" width="${W-padL-padR}" height="${H-padT-padB}" rx="10" fill="url(#${svgId}-fade)" opacity=".7"/>`;
inner+=`<rect x="${padL}" y="${padT}" width="${W-padL-padR}" height="${H-padT-padB}" rx="12" fill="url(#${svgId}-panel)" stroke="rgba(255,255,255,.08)"/>`;
for(const v of yTicks){
const yy=y(v).toFixed(1);
inner+=`<line x1="${padL}" x2="${W-padR}" y1="${yy}" y2="${yy}" stroke="rgba(255,255,255,.08)" stroke-width="1"/>`;
inner+=`<text x="${padL-8}" y="${(+yy+4).toFixed(1)}" text-anchor="end" fill="rgba(224,234,255,.62)" font-size="10" font-family="Inter, sans-serif">${fmtPct(v)}</text>`;
inner+=`<line x1="${padL}" x2="${W-padR}" y1="${yy}" y2="${yy}" stroke="rgba(255,255,255,.075)" stroke-width="1"/>`;
inner+=`<text x="${padL-12}" y="${(+yy+4).toFixed(1)}" text-anchor="end" fill="rgba(224,234,255,.62)" font-size="11" font-family="Inter, sans-serif">${fmtPct(v)}</text>`;
}
inner+=`<line x1="${padL}" x2="${W-padR}" y1="${zeroY}" y2="${zeroY}" stroke="rgba(255,255,255,.24)" stroke-width="1"/>`;
inner+=`<line x1="${padL}" x2="${W-padR}" y1="${zeroY}" y2="${zeroY}" stroke="rgba(255,255,255,.28)" stroke-width="1.2"/>`;
for(const tick of chartTicks(allTimes,5)){
const xx=x(tick.i).toFixed(1);
inner+=`<line x1="${xx}" x2="${xx}" y1="${padT}" y2="${H-padB}" stroke="rgba(255,255,255,.07)" stroke-width="1"/>`;
inner+=`<text x="${xx}" y="${H-14}" text-anchor="middle" fill="rgba(224,234,255,.72)" font-size="10.5" font-family="Inter, sans-serif">${chartDateLabel(tick.t,true)}</text>`;
inner+=`<line x1="${xx}" x2="${xx}" y1="${padT}" y2="${H-padB}" stroke="rgba(255,255,255,.055)" stroke-width="1"/>`;
inner+=`<text x="${xx}" y="${H-18}" text-anchor="middle" fill="rgba(224,234,255,.72)" font-size="11" font-family="Inter, sans-serif">${chartDateLabel(tick.t,true)}</text>`;
}
for(const ln of lines){
const d=ln.pts.map((v,i)=>`${i===0?"M":"L"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
const dot=allTimes.length===1?`<circle cx="${x(0).toFixed(1)}" cy="${y(ln.pts[0]).toFixed(1)}" r="4" fill="${ln.c.color}"/>`:"";
inner+=`<path d="${d}" fill="none" stroke="${ln.c.color}" stroke-width="2.4" opacity=".95"/>${dot}`;
const points=ln.pts.map((v,i)=>({x:x(i).toFixed(1),y:y(v).toFixed(1)}));
const d=smoothPath(points);
const last=points[points.length-1];
inner+=`<path d="${d}" fill="none" stroke="${ln.c.color}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity=".92" filter="url(#${svgId}-soft)"/>`;
inner+=`<circle cx="${last.x}" cy="${last.y}" r="4.2" fill="${ln.c.color}" stroke="#101827" stroke-width="2"/>`;
}
const avgD=avgPts.map((v,i)=>`${i===0?"M":"L"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
inner+=`<path d="${avgD}" fill="none" stroke="#ffffff" stroke-width="2.7" stroke-dasharray="7 6" opacity=".78"/>`;
inner+=`<text x="${padL}" y="13" fill="rgba(224,234,255,.58)" font-size="10.5" font-family="Inter, sans-serif">${chartDateLabel(allTimes[0])}</text>`;
inner+=`<text x="${W-padR}" y="13" text-anchor="end" fill="rgba(224,234,255,.58)" font-size="10.5" font-family="Inter, sans-serif">${chartDateLabel(allTimes[allTimes.length-1])}</text>`;
const avgPoints=avgPts.map((v,i)=>({x:x(i).toFixed(1),y:y(v).toFixed(1)}));
inner+=`<path d="${smoothPath(avgPoints)}" fill="none" stroke="#ffffff" stroke-width="3.2" stroke-dasharray="8 7" stroke-linecap="round" opacity=".82"/>`;
inner+=`<text x="${padL}" y="21" fill="rgba(224,234,255,.64)" font-size="11.5" font-family="Inter, sans-serif">Return %</text>`;
inner+=`<text x="${W-padR}" y="21" text-anchor="end" fill="rgba(224,234,255,.58)" font-size="11.5" font-family="Inter, sans-serif">${chartDateLabel(allTimes[0])} - ${chartDateLabel(allTimes[allTimes.length-1])}</text>`;
svg.innerHTML=inner;
if($(legendId)){
const b=board();const avg=b.reduce((s,r)=>s+r.ret,0)/b.length;