// Real SOL price chart renderer (data snapshot from Surf API) import { SOL_PRICES, SOL_T0 } from './data-sol.js'; import { clamp, lerp, glow, text, hexA, fmtUSD, F_MONO, GREEN, PURPLE, RED, WHITE } from './draw.js'; export const idx = (date) => clamp(Math.round((Date.parse(date + 'T00:00:00Z') / 1000 - SOL_T0) / 86400), 0, SOL_PRICES.length - 1); export const price = (i) => SOL_PRICES[clamp(Math.round(i), 0, SOL_PRICES.length - 1)]; export const dateOf = (i) => new Date((SOL_T0 + Math.round(i) * 86400) * 1000).toISOString().slice(0, 10); export const LAST = SOL_PRICES.length - 1; /** interpolated price at fractional index */ export function priceF(f) { const i = Math.floor(f), k = f - i; return lerp(price(i), price(i + 1), k); } /** * Draw a chart. rect {x,y,w,h}. from/to: day indices. prog: 0..1 drawn fraction. * Returns head {x,y,v,i,slope}. */ export function drawChart(ctx, rect, o) { const { from, to, prog = 1, log = false, color = GREEN, color2 = PURPLE, alpha = 1, grid = true, yMin, yMax, lw = 5, fill = true, head = true, headLabel = true, dates = true, gridVals } = o; const n = to - from; let lo = Infinity, hi = -Infinity; for (let i = from; i <= to; i++) { const v = price(i); if (v < lo) lo = v; if (v > hi) hi = v; } lo = yMin ?? lo * (log ? .8 : .9); hi = yMax ?? hi * 1.08; if (!log) lo = yMin ?? Math.max(0, lo - (hi - lo) * .05); const Y = v => log ? rect.y + rect.h - (Math.log(v) - Math.log(lo)) / (Math.log(hi) - Math.log(lo)) * rect.h : rect.y + rect.h - (v - lo) / (hi - lo) * rect.h; const X = i => rect.x + (i - from) / n * rect.w; ctx.save(); ctx.globalAlpha *= alpha; if (grid) { const vals = gridVals || (log ? [1, 10, 100] : niceTicks(lo, hi)); ctx.lineWidth = 1; for (const v of vals) { if (v < lo || v > hi) continue; const y = Y(v); ctx.strokeStyle = 'rgba(255,255,255,.09)'; ctx.setLineDash([6, 8]); ctx.beginPath(); ctx.moveTo(rect.x, y); ctx.lineTo(rect.x + rect.w, y); ctx.stroke(); ctx.setLineDash([]); text(ctx, fmtUSD(v), rect.x - 12, y, { size: 20, fam: F_MONO, weight: 500, color: 'rgba(255,255,255,.45)', align: 'right' }); } if (dates) { const y0 = new Date((SOL_T0 + from * 86400) * 1000).getUTCFullYear(), y1 = new Date((SOL_T0 + to * 86400) * 1000).getUTCFullYear(); for (let yr = y0; yr <= y1 + 1; yr++) { const i = idx(yr + '-01-01'); if (i <= from || i >= to) continue; const x = X(i); ctx.strokeStyle = 'rgba(255,255,255,.08)'; ctx.beginPath(); ctx.moveTo(x, rect.y); ctx.lineTo(x, rect.y + rect.h); ctx.stroke(); text(ctx, String(yr), x, rect.y + rect.h + 22, { size: 20, fam: F_MONO, weight: 600, color: 'rgba(255,255,255,.5)', align: 'center' }); } } } const endF = from + n * clamp(prog); if (endF > from) { const step = Math.max(1, Math.floor(n / 900)); ctx.beginPath(); let first = true, lx = 0, ly = 0; for (let i = from; i <= Math.floor(endF); i += step) { const x = X(i), y = Y(price(i)); if (first) { ctx.moveTo(x, y); first = false; } else ctx.lineTo(x, y); lx = x; ly = y; } const hv = priceF(endF), hx = X(endF), hy = Y(hv); ctx.lineTo(hx, hy); const g = ctx.createLinearGradient(rect.x, 0, rect.x + rect.w, 0); g.addColorStop(0, color2); g.addColorStop(1, color); // fill if (fill) { ctx.save(); ctx.lineTo(hx, rect.y + rect.h); ctx.lineTo(X(from), rect.y + rect.h); ctx.closePath(); const fg = ctx.createLinearGradient(0, rect.y, 0, rect.y + rect.h); fg.addColorStop(0, hexA(color, .28)); fg.addColorStop(1, hexA(color, 0)); ctx.fillStyle = fg; ctx.fill(); ctx.restore(); // re-trace stroke path ctx.beginPath(); first = true; for (let i = from; i <= Math.floor(endF); i += step) { const x = X(i), y = Y(price(i)); if (first) { ctx.moveTo(x, y); first = false; } else ctx.lineTo(x, y); } ctx.lineTo(hx, hy); } ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.strokeStyle = hexA(color, .25); ctx.lineWidth = lw * 3.2; ctx.stroke(); ctx.strokeStyle = g; ctx.lineWidth = lw; ctx.stroke(); void lx; void ly; const sl = (Y(priceF(Math.min(endF + 3, to))) - Y(priceF(Math.max(endF - 3, from)))) / (X(Math.min(endF + 3, to)) - X(Math.max(endF - 3, from)) || 1); if (head && prog > 0) { glow(ctx, hx, hy, 60, color, .9); ctx.fillStyle = WHITE; ctx.beginPath(); ctx.arc(hx, hy, 8, 0, 7); ctx.fill(); if (headLabel) { const right = hx < rect.x + rect.w * .75; text(ctx, fmtUSD(hv), hx + (right ? 22 : -22), hy - 34, { size: 34, fam: F_MONO, weight: 800, color: WHITE, align: right ? 'left' : 'right', stroke: 6, strokeColor: 'rgba(0,0,0,.7)' }); text(ctx, dateOf(endF), hx + (right ? 22 : -22), hy - 2, { size: 20, fam: F_MONO, weight: 500, color: 'rgba(255,255,255,.6)', align: right ? 'left' : 'right' }); } } ctx.restore(); return { x: hx, y: hy, v: hv, i: endF, slope: sl, X, Y }; } ctx.restore(); return { x: X(from), y: Y(price(from)), v: price(from), i: from, slope: 0, X, Y }; } function niceTicks(lo, hi) { const span = hi - lo, raw = span / 4, mag = Math.pow(10, Math.floor(Math.log10(raw))); const step = [1, 2, 2.5, 5, 10].map(k => k * mag).find(s => span / s <= 5); const out = []; for (let v = Math.ceil(lo / step) * step; v <= hi; v += step) out.push(+v.toFixed(4)); return out; } export const RED_C = RED;