Fetch real-time and historical stock data from Finnhub and render interactive candlestick charts in the browser. 100% client-side, zero backend required.
Enter your Finnhub API key and a ticker symbol to fetch real market data. Use the chart's built-in timeframe selector to switch between intraday, daily, weekly, and monthly views — new data is automatically fetched from Finnhub when you change timeframes.
Everything you need to get Finnhub data into a chart
Head to finnhub.io/register and create a free account. Once logged in, your API key is displayed on the dashboard. The free tier gives you 60 API calls per minute with access to real-time US stock data, company fundamentals, and more.
Add the JavaScript Stock Charts library to your HTML page via CDN. It's zero-dependency — just three script tags and one CSS link. No downloads or installs required.
<!-- CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/day-trading-simulator/javascript-stock-charts@main/stock-chart.css">
<!-- Chart container -->
<div id="chart" style="width: 100%; height: 500px;"></div>
<!-- JS (order matters) -->
<script src="https://cdn.jsdelivr.net/gh/day-trading-simulator/javascript-stock-charts@main/indicators.js"></script>
<script src="https://cdn.jsdelivr.net/gh/day-trading-simulator/javascript-stock-charts@main/patterns.js"></script>
<script src="https://cdn.jsdelivr.net/gh/day-trading-simulator/javascript-stock-charts@main/stock-chart.js"></script>
Use the Stock Candles endpoint to fetch OHLCV data. This works directly in the browser — Finnhub supports CORS on their REST API. Note that Finnhub uses UNIX timestamps in seconds (not milliseconds) for the from and to parameters.
const API_KEY = 'YOUR_FINNHUB_API_KEY';
const ticker = 'AAPL';
const to = Math.floor(Date.now() / 1000); // now (unix seconds)
const from = to - (365 * 24 * 60 * 60); // 1 year ago
const url = `https://finnhub.io/api/v1/stock/candle?symbol=${ticker}&resolution=D&from=${from}&to=${to}&token=${API_KEY}`;
const response = await fetch(url);
const json = await response.json();
Finnhub returns data as parallel arrays — each field is a separate array rather than an array of objects:
{
"s": "ok",
"c": [185.56, 184.25, ...], // close prices
"h": [188.44, 185.88, ...], // high prices
"l": [183.89, 183.43, ...], // low prices
"o": [187.15, 184.22, ...], // open prices
"t": [1704153600, 1704240000, ...], // timestamps (UNIX seconds)
"v": [58414460, 47299890, ...] // volume
}
| Finnhub Field | Description | Chart Field |
|---|---|---|
o[] | Open prices array | open |
h[] | High prices array | high |
l[] | Low prices array | low |
c[] | Close prices array | close |
v[] | Volume array | volume |
t[] | Timestamps (UNIX seconds) | t (ms) |
Convert Finnhub's parallel arrays into an array of objects that the chart library expects. Loop through the arrays by index and multiply timestamps by 1000 to convert from seconds to milliseconds:
function transformFinnhubData(json) {
var data = [];
for (var i = 0; i < json.t.length; i++) {
var ts = json.t[i] * 1000; // seconds -> milliseconds
var d = new Date(ts);
var date = d.toLocaleString('en-US', {
timeZone: 'America/New_York',
year: 'numeric', month: 'numeric', day: 'numeric',
hour: '2-digit', minute: '2-digit', hour12: false
}).replace(',', '');
data.push({
t: ts, date: date, open: json.o[i], high: json.h[i],
low: json.l[i], close: json.c[i], volume: json.v[i]
});
}
return data;
}
const chartData = transformFinnhubData(json);
Create a StockChart instance with an onReachingStart callback for infinite scroll. Then monitor the chart's built-in timeframe selector to re-fetch data when the user switches timeframes:
var chart = null;
var previousTimeframe = '1day';
var isLoadingMore = false;
var earliestTimestamp = null;
var hasMoreHistory = true;
var timeframeCheckInterval = null;
// Map chart timeframes to Finnhub resolution values
function mapTimeframeToFinnhubParams(tf) {
var map = {
'1min': { resolution: '1', seconds: 5 * 24 * 3600 },
'5min': { resolution: '5', seconds: 5 * 24 * 3600 },
'15min': { resolution: '15', seconds: 10 * 24 * 3600 },
'1hour': { resolution: '60', seconds: 30 * 24 * 3600 },
'1day': { resolution: 'D', seconds: 365 * 24 * 3600 },
'1week': { resolution: 'W', seconds: 1095 * 24 * 3600 },
'1month':{ resolution: 'M', seconds: 1825 * 24 * 3600 }
};
return map[tf] || map['1day'];
}
// Initialize chart
chart = new StockChart('chart', {
data: chartData,
ticker: ticker,
chartType: 'candlestick',
darkMode: true,
timeframe: '1day',
useAfterHoursStyling: true,
onReachingStart: handleReachingStart
});
earliestTimestamp = chartData[0].t;
// Monitor chart's built-in timeframe selector
setupTimeframeMonitor();
function setupTimeframeMonitor() {
if (timeframeCheckInterval) clearInterval(timeframeCheckInterval);
timeframeCheckInterval = setInterval(function() {
if (chart && chart.timeframe !== previousTimeframe) {
previousTimeframe = chart.timeframe;
loadChart(); // re-fetch with new timeframe
}
}, 500);
}
// Infinite scroll - fetch older data when user pans left
async function handleReachingStart() {
if (isLoadingMore || !hasMoreHistory || !earliestTimestamp) return;
isLoadingMore = true;
try {
var p = mapTimeframeToFinnhubParams(chart.timeframe || '1day');
var toSec = Math.floor(earliestTimestamp / 1000) - 1;
var fromSec = toSec - p.seconds;
var url = 'https://finnhub.io/api/v1/stock/candle?symbol=' + ticker
+ '&resolution=' + p.resolution
+ '&from=' + fromSec + '&to=' + toSec
+ '&token=' + API_KEY;
var resp = await fetch(url);
var json = await resp.json();
if (json.s === 'ok' && json.t && json.t.length > 0) {
var older = transformFinnhubData(json);
earliestTimestamp = older[0].t;
chart.prependHistoricalData(older);
} else {
hasMoreHistory = false;
chart.setLoadingHistoricalData(false);
}
} catch(e) {
chart.setLoadingHistoricalData(false);
} finally {
isLoadingMore = false;
}
}
The chart includes a built-in timeframe dropdown — when the user clicks it, we detect the change via setInterval polling on chart.timeframe and automatically re-fetch data from Finnhub. The onReachingStart callback fires when the user pans to the beginning of loaded data, triggering a fetch of older candles that get prepended seamlessly. Technical indicators, pan, zoom, crosshair, volume, and more are all built-in.
Copy-paste this entire HTML file. Replace YOUR_FINNHUB_API_KEY with your key, open it in a browser, and you'll have a working stock chart in seconds.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Finnhub + JavaScript Stock Charts</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/day-trading-simulator/javascript-stock-charts@main/stock-chart.css">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
padding: 20px;
background: #0a0a0f;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: #fff;
}
h1 { font-size: 1.4rem; margin-bottom: 4px; }
.subtitle { color: #888; margin-bottom: 16px; }
.subtitle a { color: #22d3ee; }
.controls {
display: flex; gap: 10px; margin-bottom: 12px;
flex-wrap: wrap; align-items: flex-end;
}
.controls label {
display: block; font-size: 0.7rem;
text-transform: uppercase; letter-spacing: 1px;
color: #888; margin-bottom: 4px;
}
.controls input {
background: #16161f; border: 1px solid #333;
color: #fff; padding: 8px 12px; font-size: 0.9rem;
border-radius: 2px;
}
.controls button {
background: #10b981; border: none; color: #fff;
padding: 8px 24px; font-weight: 700; cursor: pointer;
border-radius: 2px;
}
.controls button:hover { background: #059669; }
.sc-chart-type-button.d-none { display: inline-flex !important; }
#status { font-size: 0.85rem; color: #888; margin-bottom: 8px; }
#status.error { color: #ef4444; }
#status.success { color: #10b981; }
</style>
</head>
<body>
<h1>Finnhub + JavaScript Stock Charts</h1>
<p class="subtitle">
Powered by <a href="https://simul8or.com/Javascript-Stock-Chart.php">JavaScript Stock Charts</a>
</p>
<div class="controls">
<div>
<label>API Key</label>
<input type="text" id="apiKey" placeholder="Your Finnhub API key" style="width:280px">
</div>
<div>
<label>Ticker</label>
<input type="text" id="ticker" value="AAPL" style="width:100px">
</div>
<button onclick="loadChart()">Load Chart</button>
</div>
<div id="status"></div>
<div id="chart" style="width:100%; height:500px;"></div>
<script src="https://cdn.jsdelivr.net/gh/day-trading-simulator/javascript-stock-charts@main/indicators.js"></script>
<script src="https://cdn.jsdelivr.net/gh/day-trading-simulator/javascript-stock-charts@main/patterns.js"></script>
<script src="https://cdn.jsdelivr.net/gh/day-trading-simulator/javascript-stock-charts@main/stock-chart.js"></script>
<script>
var chart = null;
var currentTicker = 'AAPL';
var previousTimeframe = '1day';
var timeframeCheckInterval = null;
var isLoadingMore = false;
var earliestTimestamp = null;
var hasMoreHistory = true;
// Map chart timeframes to Finnhub resolution values
function mapTimeframeToFinnhubParams(tf) {
var map = {
'1min': { resolution: '1', seconds: 5 * 24 * 3600 },
'2min': { resolution: '5', seconds: 5 * 24 * 3600 },
'5min': { resolution: '5', seconds: 5 * 24 * 3600 },
'15min': { resolution: '15', seconds: 10 * 24 * 3600 },
'30min': { resolution: '30', seconds: 15 * 24 * 3600 },
'1hour': { resolution: '60', seconds: 30 * 24 * 3600 },
'60min': { resolution: '60', seconds: 30 * 24 * 3600 },
'4hour': { resolution: '60', seconds: 90 * 24 * 3600 },
'1day': { resolution: 'D', seconds: 365 * 24 * 3600 },
'1week': { resolution: 'W', seconds: 1095 * 24 * 3600 },
'1W': { resolution: 'W', seconds: 1095 * 24 * 3600 },
'1month': { resolution: 'M', seconds: 1825 * 24 * 3600 },
'1M': { resolution: 'M', seconds: 1825 * 24 * 3600 }
};
return map[tf] || map['1day'];
}
function transformFinnhubData(json) {
var data = [];
for (var i = 0; i < json.t.length; i++) {
var ts = json.t[i] * 1000;
var d = new Date(ts);
var date = d.toLocaleString('en-US', {
timeZone: 'America/New_York',
year: 'numeric', month: 'numeric', day: 'numeric',
hour: '2-digit', minute: '2-digit', hour12: false
}).replace(',', '');
data.push({
t: ts, date: date, open: json.o[i], high: json.h[i],
low: json.l[i], close: json.c[i], volume: json.v[i]
});
}
return data;
}
// Fetch data from Finnhub and render chart
async function loadChart() {
var apiKey = document.getElementById('apiKey').value.trim();
var ticker = document.getElementById('ticker').value.trim().toUpperCase();
var status = document.getElementById('status');
if (!apiKey) { status.textContent = 'Enter your Finnhub API key.'; status.className = 'error'; return; }
if (!ticker) { status.textContent = 'Enter a ticker symbol.'; status.className = 'error'; return; }
var timeframe = (chart && chart.timeframe) ? chart.timeframe : '1day';
var p = mapTimeframeToFinnhubParams(timeframe);
status.textContent = 'Fetching ' + timeframe + ' data...';
status.className = '';
var toSec = Math.floor(Date.now() / 1000);
var fromSec = toSec - p.seconds;
var url = 'https://finnhub.io/api/v1/stock/candle?symbol=' + ticker
+ '&resolution=' + p.resolution
+ '&from=' + fromSec + '&to=' + toSec
+ '&token=' + apiKey;
try {
var resp = await fetch(url);
var json = await resp.json();
if (json.s !== 'ok' || !json.t || json.t.length === 0) {
status.textContent = json.s === 'no_data'
? 'No data for ' + ticker + '.'
: 'Error fetching data: ' + (json.s || 'unknown');
status.className = 'error'; return;
}
var chartData = transformFinnhubData(json);
if (chart) chart.destroy();
if (timeframeCheckInterval) clearInterval(timeframeCheckInterval);
currentTicker = ticker;
chart = new StockChart('chart', {
data: chartData, ticker: ticker,
chartType: 'candlestick', darkMode: true,
timeframe: timeframe,
useAfterHoursStyling: true,
onReachingStart: handleReachingStart
});
isLoadingMore = false;
hasMoreHistory = true;
earliestTimestamp = chartData[0].t;
previousTimeframe = chart.timeframe || timeframe;
setupTimeframeMonitor();
status.textContent = 'Loaded ' + chartData.length + ' bars (' + timeframe + ') for ' + ticker + '.';
status.className = 'success';
} catch (err) {
status.textContent = 'Network error: ' + err.message;
status.className = 'error';
}
}
// Monitor chart's built-in timeframe selector for changes
function setupTimeframeMonitor() {
if (timeframeCheckInterval) clearInterval(timeframeCheckInterval);
timeframeCheckInterval = setInterval(function() {
if (chart && chart.timeframe !== previousTimeframe) {
previousTimeframe = chart.timeframe;
loadChart();
}
}, 500);
}
// Infinite scroll - fetch older data when user pans to start
async function handleReachingStart() {
if (isLoadingMore || !hasMoreHistory || !earliestTimestamp) return;
isLoadingMore = true;
var apiKey = document.getElementById('apiKey').value.trim();
if (!apiKey) { isLoadingMore = false; return; }
try {
var p = mapTimeframeToFinnhubParams(chart.timeframe || '1day');
var toSec = Math.floor(earliestTimestamp / 1000) - 1;
var fromSec = toSec - p.seconds;
var url = 'https://finnhub.io/api/v1/stock/candle?symbol=' + currentTicker
+ '&resolution=' + p.resolution
+ '&from=' + fromSec + '&to=' + toSec
+ '&token=' + apiKey;
var resp = await fetch(url);
var json = await resp.json();
if (json.s === 'ok' && json.t && json.t.length > 0) {
var older = transformFinnhubData(json);
earliestTimestamp = older[0].t;
chart.prependHistoricalData(older);
} else {
hasMoreHistory = false;
chart.setLoadingHistoricalData(false);
}
} catch(e) {
chart.setLoadingHistoricalData(false);
} finally {
isLoadingMore = false;
}
}
</script>
</body>
</html>
Download the library, grab your Finnhub API key, and start charting.
See more tutorials: JavaScript Stock Charts Documentation | Polygon.io API Tutorial