Custom chart (bring your own data)
Render a candlestick + volume chart and a technical indicator overlay directly from your own quote data - no API required. Useful for offline demos, internal dashboards, or static documentation sites.
Live demo
The chart above plots OHLC + volume from a hard-coded Quote[] array, with an EMA(20) line computed locally. Everything below ships in the page bundle - no network calls.
How it works
OverlayChart is a lower-level building block exported from @facioquo/indy-charts. Unlike <StockIndicatorChart>, it does not fetch from an API - you supply the quotes directly. Add indicators by pushing your own ChartDataset onto chart.data.datasets after render().
import { OverlayChart, loadStaticQuotes, setupIndyCharts } from '@facioquo/indy-charts'
import type { Quote } from '@facioquo/indy-charts'
const quotes: Quote[] = loadStaticQuotes([
{ timestamp: '2025-01-02', open: 180.00, high: 182.50, low: 179.20, close: 181.80, volume: 38500000 },
// ... more bars
])
setupIndyCharts() // one-time global Chart.js registration
const canvas = document.getElementById('my-canvas') as HTMLCanvasElement
const chart = new OverlayChart(canvas, { isDarkTheme: false, showTooltips: false })
chart.render(quotes)
// Push an EMA(20) line onto the existing chart.
chart.chart?.data.datasets.push(buildEmaDataset(quotes, 20))
chart.chart?.update('none')Pair the script with a simple <canvas> host element:
<div style="position: relative; aspect-ratio: 2.5;">
<canvas id="my-canvas"></canvas>
</div>Computing the EMA locally
Without an API to compute indicators server-side, you can do it inline. EMA is just a recurrence:
function computeEma(closes: number[], period: number): number[] {
if (!Number.isInteger(period) || period <= 0) {
throw new Error(`EMA period must be a positive integer, got ${period}`)
}
const k = 2 / (period + 1)
const result: number[] = new Array(closes.length).fill(NaN)
if (closes.length < period) return result
// Seed with SMA of the first `period` closes.
let sum = 0
for (let i = 0; i < period; i++) sum += closes[i]
result[period - 1] = sum / period
// Standard EMA recurrence after the seed.
for (let i = period; i < closes.length; i++) {
result[i] = closes[i] * k + result[i - 1] * (1 - k)
}
return result
}Wrap the result in a Chart.js line dataset on the price y-axis:
import type { ChartDataset, ScatterDataPoint } from 'chart.js'
import type { Quote } from '@facioquo/indy-charts'
function buildEmaDataset(quotes: Quote[], period: number): ChartDataset<'line', ScatterDataPoint[]> {
const ema = computeEma(quotes.map(q => q.close), period)
return {
type: 'line',
label: `EMA(${period})`,
data: quotes.map((q, i) => ({ x: new Date(q.timestamp).valueOf(), y: ema[i] })),
yAxisID: 'y',
borderColor: '#FFA726',
backgroundColor: '#FFA726',
borderWidth: 1.5,
pointRadius: 0,
fill: false,
spanGaps: false,
order: 0
}
}Vue component source
The full Vue 3 SFC that drives the live demo is at docs/examples/StaticChart.vue — that file is the source of truth (mount, theme observer, teardown, and EMA helpers). The page embeds it via <ClientOnly> so the Chart.js code only runs in the browser.
Key points
Quote— the OHLCV bar shape from@facioquo/indy-charts: numericopen,high,low,close,volume, plustimestamp: Date | string.loadStaticQuotesnormalizes any string timestamps toDateinstances.loadStaticQuotes(raw)— accepts raw bars whosetimestampis an ISO 8601 string orDate, normalizes them toDate, and returnsQuote[].setupIndyCharts()— one-time global Chart.js + financial-chart-type registration; safe to call multiple times.OverlayChart— renders candlestick + volume directly onto a<canvas>element.- Custom indicators — push your own
ChartDatasetontochart.data.datasetsafterrender(), then callchart.update('none'). Any Chart.js dataset shape works (line, bar, scatter, etc.). - Theme sync — re-render the chart on
document.documentElementclass changes to follow the page's dark/light mode. Guard against re-rendering for unrelated<html>class mutations (VitePress toggles other classes too). - Cleanup — call
overlayChart.destroy()from your unmount hook; it releases the wrapper state and the underlying Chart.js instance in one call.
✨ Tip: direct lower-level use is opt-in
For most pages you should use the higher-level <StockIndicatorChart> from @facioquo/indy-charts/vue (registered globally in .vitepress/theme/index.ts). It fetches quotes + indicators from the configured API, manages its own lifecycle, and respects the central indicator catalog. Drop down to OverlayChart only when you genuinely need to ship data inline.