Create a single HTML file that displays a live, auto-updating Bitcoin (BTC/USDT) price chart. Requirements: - **Single file**: All HTML, CSS, and JavaScript in one file (CDN links are fine) - **Data source**: Binance WebSocket API (`wss://stream.binance.com:9443/ws/btcusdt@trade`) for real-time price data - **Charting**: Use Chart.js (or similar lightweight library via CDN) to render a clean line chart - **Live updates**: Chart should update in real-time as new trades come in (throttle updates to ~1 per second to keep it smooth) - **Minimal design**: Dark theme, clean typography, show current price prominently above the chart - **Time window**: Display the last 5 minutes of price data, with old points rolling off - **Responsive**: Should look good on both desktop and mobile - Keep it simple, elegant, and under 200 lines if possible
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>BTC/USDT Live (Binance)</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1"></script>
<style>
:root{
--bg:#0b0f14; --panel:#0f1620; --text:#e6edf3; --muted:#8aa0b5; --accent:#4cc9f0;
--grid:rgba(255,255,255,.06); --line:rgba(76,201,240,.9);
}
*{box-sizing:border-box}
body{
margin:0; font:14px/1.4 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;
background:radial-gradient(1200px 600px at 20% 0%, #101a26 0%, var(--bg) 55%);
color:var(--text);
display:flex; min-height:100vh; align-items:center; justify-content:center; padding:18px;
}
.wrap{width:min(980px,100%); background:linear-gradient(180deg,var(--panel),#0b111a);
border:1px solid rgba(255,255,255,.06); border-radius:16px; padding:16px 16px 12px;
box-shadow:0 10px 30px rgba(0,0,0,.35);
}
header{display:flex; align-items:baseline; justify-content:space-between; gap:12px; padding:4px 4px 10px}
.title{font-weight:650; letter-spacing:.2px}
.sub{color:var(--muted); font-size:12px}
.price{font-size:34px; font-weight:750; letter-spacing:.2px}
.row{display:flex; align-items:baseline; gap:10px; flex-wrap:wrap}
.pill{font-size:12px; color:var(--muted); padding:4px 10px; border:1px solid rgba(255,255,255,.08);
border-radius:999px; background:rgba(255,255,255,.03)
}
.chartBox{height:380px; position:relative}
canvas{width:100% !important; height:100% !important}
@media (max-width:560px){ .price{font-size:28px} .chartBox{height:320px} }
</style>
</head>
<body>
<div class="wrap">
<header>
<div>
<div class="title">BTC/USDT <span class="sub">live trades (Binance WebSocket)</span></div>
<div class="row">
<div class="price" id="price">—</div>
<div class="pill" id="status">connecting…</div>
</div>
</div>
<div class="sub">Last 5 minutes</div>
</header>
<div class="chartBox"><canvas id="c"></canvas></div>
</div>
<script>
(() => {
const WS_URL = "wss://stream.binance.com:9443/ws/btcusdt@trade";
const WINDOW_MS = 5 * 60 * 1000;
const THROTTLE_MS = 1000;
const elPrice = document.getElementById("price");
const elStatus = document.getElementById("status");
const fmtUSD = new Intl.NumberFormat(undefined, { style:"currency", currency:"USD", maximumFractionDigits:2 });
const fmtTime = ts => new Date(ts).toLocaleTimeString([], { hour:"2-digit", minute:"2-digit", second:"2-digit" });
const ctx = document.getElementById("c").getContext("2d");
const data = { labels: [], datasets: [{
label: "BTC/USDT",
data: [],
borderColor: getComputedStyle(document.documentElement).getPropertyValue("--line").trim(),
backgroundColor: "rgba(76,201,240,.08)",
fill: true,
tension: 0.25,
pointRadius: 0,
borderWidth: 2
}]};
const chart = new Chart(ctx, {
type: "line",
data,
options: {
responsive: true,
maintainAspectRatio: false,
animation: false,
interaction: { mode: "index", intersect: false },
plugins: {
legend: { display: false },
tooltip: {
displayColors: false,
callbacks: { label: (c) => `${fmtUSD.format(c.parsed.y)}` }
}
},
scales: {
x: {
grid: { color: "rgba(255,255,255,.06)" },
ticks: { color: "rgba(230,237,243,.7)", maxTicksLimit: 6 }
},
y: {
grid: { color: "rgba(255,255,255,.06)" },
ticks: {
color: "rgba(230,237,243,.7)",
callback: v => fmtUSD.format(v)
}
}
}
}
});
let ws, lastPrice = null, lastTs = 0, pending = false;
function setStatus(text){ elStatus.textContent = text; }
function pushPoint(ts, price){
data.labels.push(fmtTime(ts));
data.datasets[0].data.push({ x: ts, y: price });
const cutoff = ts - WINDOW_MS;
while (data.datasets[0].data.length && data.datasets[0].data[0].x < cutoff) {
data.datasets[0].data.shift();
data.labels.shift();
}
}
function render(){
pending = false;
if (lastPrice == null) return;
const now = Date.now();
pushPoint(now, lastPrice);
elPrice.textContent = fmtUSD.format(lastPrice);
chart.update("none");
}
function scheduleRender(){
if (pending) return;
const now = Date.now();
const wait = Math.max(0, THROTTLE_MS - (now - lastTs));
pending = true;
setTimeout(() => { lastTs = Date.now(); render(); }, wait);
}
function connect(){
setStatus("connecting…");
ws = new WebSocket(WS_URL);
ws.onopen = () => setStatus("live");
ws.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data);
lastPrice = +msg.p; // trade price as string -> number
scheduleRender(); // throttle to ~1 update/sec
} catch {}
};
ws.onclose = () => { setStatus("reconnecting…"); setTimeout(connect, 800); };
ws.onerror = () => { try{ ws.close(); } catch {} };
}
// Seed chart with a flat line until first tick arrives
const t0 = Date.now();
for (let i = 5; i >= 1; i--) pushPoint(t0 - i*1000, NaN);
chart.update("none");
connect();
})();
</script>
</body>
</html>
Comments