/* ============================================================================ StonksScout — CHART PRIMITIVES (hand-rolled SVG, no chart deps) ============================================================================ */ const { useId } = React; /* ---- Conviction bar (signature 0–100 readout) ----------------------------- */ function ConvictionBar({ value, color, height=10, showScale=false }){ const pct = Math.max(0, Math.min(100, value)); return (
{[25,50,75].map(t=>(
))}
{showScale && (
050100
)}
); } /* ---- Big conviction gauge (detail view) — 270° arc ------------------------ */ function ConvictionGauge({ value, color, size=190, label }){ const pct = Math.max(0, Math.min(100, value)); const r = size/2 - 16, cx = size/2, cy = size/2; const start = 135, sweep = 270; const polar = (deg) => { const a = (deg) * Math.PI/180; return [cx + r*Math.cos(a), cy + r*Math.sin(a)]; }; const arc = (fromPct, toPct) => { const a0 = start + (sweep*fromPct/100), a1 = start + (sweep*toPct/100); const [x0,y0] = polar(a0), [x1,y1] = polar(a1); const large = (a1-a0) > 180 ? 1 : 0; return `M ${x0} ${y0} A ${r} ${r} 0 ${large} 1 ${x1} ${y1}`; }; return ( {Math.round(pct)} {label || "CONVICTION"} ); } /* ---- Radar / spider chart (8 factors) ------------------------------------- */ function RadarChart({ factors, color, size=300 }){ const keys = Object.keys(factors); const n = keys.length, cx=size/2, cy=size/2, R=size/2-46; const pt = (i, rad) => { const a = (-90 + i*360/n) * Math.PI/180; return [cx + rad*Math.cos(a), cy + rad*Math.sin(a)]; }; const rings = [0.25,0.5,0.75,1]; const poly = (vals) => vals.map((v,i)=>pt(i, R*v/100).join(",")).join(" "); const gridPoly = (f) => keys.map((_,i)=>pt(i, R*f).join(",")).join(" "); return ( {rings.map((f,i)=>( ))} {keys.map((k,i)=>{ const [x,y]=pt(i,R); return ; })} factors[k]))} fill={color+"33"} stroke={color} strokeWidth="2" style={{ filter:`drop-shadow(0 0 8px ${color}55)` }} /> {keys.map((k,i)=>{ const [x,y]=pt(i, R*factors[k]/100); return ; })} {keys.map((k,i)=>{ const [x,y]=pt(i, R+24); const lbl = FACTOR_LABELS[k] || k; return ( {lbl.split(" ")[0]} {lbl.split(" ")[1] && {lbl.split(" ")[1]}} ); })} ); } /* ---- Labeled factor bars (compact, used in cards) ------------------------- */ function FactorBars({ factors, color, compact=false }){ const keys = Object.keys(factors); return (
{keys.map(k=>(
{(FACTOR_LABELS[k]||k).replace(" ","\u00A0")}
{factors[k]}
))}
); } /* ---- Sparkline (price history) -------------------------------------------- */ function Sparkline({ data, color, width=120, height=34, strokeWidth=1.6, fill=true }){ const gid = useId().replace(/:/g,""); if(!data || data.length<2) return ; const min = Math.min(...data), max = Math.max(...data), span = (max-min)||1; const x = (i) => (i/(data.length-1))*(width-2)+1; const y = (v) => height-2 - ((v-min)/span)*(height-4); const pts = data.map((v,i)=>[x(i),y(v)]); const line = pts.map((p,i)=>(i?"L":"M")+p[0].toFixed(1)+" "+p[1].toFixed(1)).join(" "); const area = line + ` L ${width-1} ${height} L 1 ${height} Z`; return ( {fill && } ); } /* ---- Trend chip (tiny conviction trend with delta) ------------------------ */ function TrendSpark({ data, width=58, height=20 }){ const delta = data[data.length-1]-data[0]; const c = delta>=0 ? "#17a86a" : "#ee3f5f"; return (
{delta>=0?"+":""}{delta}
); } /* ---- Price chart with entry / stop / target lines (detail view) ----------- */ function PriceChart({ data, frame, color, width=560, height=220 }){ const gid = useId().replace(/:/g,""); const padL=8, padR=64, padT=14, padB=18; const levels = []; if(frame){ if(frame.stop!=null) levels.push({v:frame.stop, c:"#ee3f5f", t:"STOP"}); (frame.targets||[]).forEach((t,i)=>levels.push({v:t, c:"#17a86a", t:"T"+(i+1)})); } const allVals = [...data, ...levels.map(l=>l.v)]; const min = Math.min(...allVals)*0.985, max = Math.max(...allVals)*1.015, span=(max-min)||1; const x = (i)=> padL + (i/(data.length-1))*(width-padL-padR); const y = (v)=> padT + (1-(v-min)/span)*(height-padT-padB); const pts = data.map((v,i)=>[x(i),y(v)]); const line = pts.map((p,i)=>(i?"L":"M")+p[0].toFixed(1)+" "+p[1].toFixed(1)).join(" "); const area = line + ` L ${x(data.length-1)} ${height-padB} L ${padL} ${height-padB} Z`; return ( {levels.map((l,i)=>( {l.t} {Number(l.v).toFixed(2)} ))} ); } /* ---- Analyst rating split (stacked bar) ----------------------------------- */ function AnalystSplit({ a }){ const segs = [ { k:"Strong Buy", v:a.strong_buy, c:"#15bd80" }, { k:"Buy", v:a.buy, c:"#17a86a" }, { k:"Hold", v:a.hold, c:"#b9870c" }, { k:"Sell", v:a.sell, c:"#ee3f5f" }, ]; const total = segs.reduce((s,x)=>s+x.v,0)||1; return (
{segs.map(s=> s.v>0 && (
))}
{segs.map(s=>(
{s.k} {s.v}
))}
); } Object.assign(window, { ConvictionBar, ConvictionGauge, RadarChart, FactorBars, Sparkline, TrendSpark, PriceChart, AnalystSplit });