Custom data (bring your own)
Render a candlestick + volume chart and a technical indicator overlay directly from your own quote data — no API required.
Live demo
The chart above plots OHLC + volume from a hard-coded Quote[] array (ISO string timestamps normalized to Date via loadStaticQuotes), 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 data from an API — you supply the quotes directly. Add indicators by pushing your own ChartDataset onto chart.data.datasets after render().
import { OverlayChart, loadStaticQuotes } from "@facioquo/indy-charts";
import type { Quote } from "@facioquo/indy-charts";
// Quote.timestamp accepts ISO strings or Date instances.
const quotes: Quote[] = loadStaticQuotes([
{ timestamp: "2025-01-02", open: 180.00, high: 182.50, low: 179.20, close: 181.80, volume: 38500000 },
// ... more bars
]);
const canvas = document.getElementById("my-canvas") as HTMLCanvasElement;
const chart = new OverlayChart(canvas, {
isDarkTheme: false,
showTooltips: false,
showRightAxisLabels: true // Optional: set to false to hide right-axis tick labels
});
chart.render(quotes);
// Push an EMA(20) line onto the existing chart.
chart.chart?.data.datasets.push(buildEmaDataset(quotes, 20));
chart.chart?.update("none");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;
}Then wrap the result into a Chart.js line dataset on the price y-axis:
import type { ChartDataset, ScatterDataPoint } from "chart.js";
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
};
}Source code
The full Vue component driving the demo above:
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue";
import {
OverlayChart,
loadStaticQuotes,
setupIndyCharts,
type Quote
} from "@facioquo/indy-charts";
import type { ChartDataset, ScatterDataPoint } from "chart.js";
// Quote.timestamp accepts ISO strings or Date instances.
const quotes: Quote[] = loadStaticQuotes([
{ timestamp: "2025-01-02", open: 180.00, high: 182.50, low: 179.20, close: 181.80, volume: 38500000 },
// ... more bars
]);
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;
let sum = 0;
for (let i = 0; i < period; i++) sum += closes[i];
result[period - 1] = sum / period;
for (let i = period; i < closes.length; i++) {
result[i] = closes[i] * k + result[i - 1] * (1 - k);
}
return result;
}
function buildEmaDataset(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
};
}
const canvasEl = ref<HTMLCanvasElement | null>(null);
let overlayChart: OverlayChart | null = null;
let observer: MutationObserver | null = null;
function isDark() {
return document.documentElement.classList.contains("dark");
}
function renderChart() {
if (!canvasEl.value) return;
setupIndyCharts();
overlayChart?.destroy();
overlayChart = new OverlayChart(canvasEl.value, {
isDarkTheme: isDark(),
showTooltips: false,
showRightAxisLabels: false, // Cleaner look for documentation examples
background: isDark() ? "#1b1b1f80" : "#ffffff80"
});
overlayChart.render(quotes);
overlayChart.chart?.data.datasets.push(buildEmaDataset(20));
overlayChart.chart?.update("none");
}
onMounted(() => {
renderChart();
observer = new MutationObserver(() => renderChart());
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"]
});
});
onBeforeUnmount(() => {
observer?.disconnect();
observer = null;
overlayChart?.destroy();
overlayChart = null;
});
</script>
<template>
<canvas ref="canvasEl" />
</template>Key points
Quote: single OHLCV bar —timestampaccepts an ISO string orDateinstance, the rest are numeric. Type your fixture arrays asQuote[].loadStaticQuotes: normalizesQuote.timestampto aDate(no-op when already a Date)OverlayChart: renders candlestick and 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, dot, bar, etc. - Theme sync: re-create the chart on
document.documentElementclass changes to follow the page's dark / light mode automatically - Cleanup: always call
chart.destroy()(the wrapper) in the component's unmount hook — neverchart.chart?.destroy()(Chart.js only), which leaks the wrapper's cached state
Next steps
- Return to the basic chart example
- See an oscillator paired with overlay
- Read the API client reference