Working with data in TypeScript Stock Chart
Stock Chart can visualize data bound from local or remote data.
Local Data
You can bind a simple JSON data to the chart using dataSource property in series.
import { StockChart } from '@syncfusion/ej2-charts';
import { chartData } from './datasource.ts';
import { DateTime, HiloOpenCloseSeries, CandleSeries } from '@syncfusion/ej2-charts';
import { AccumulationDistributionIndicator, AtrIndicator, BollingerBands, EmaIndicator, MomentumIndicator } from '@syncfusion/ej2-charts';
import { MacdIndicator, RsiIndicator, Trendlines, SmaIndicator, StochasticIndicator, Export } from '@syncfusion/ej2-charts';
import { TmaIndicator, RangeTooltip, Tooltip, Crosshair}
from '@syncfusion/ej2-charts';
StockChart.Inject(DateTime, HiloOpenCloseSeries, CandleSeries);
StockChart.Inject(AccumulationDistributionIndicator, AtrIndicator, BollingerBands, EmaIndicator, MomentumIndicator);
StockChart.Inject(MacdIndicator, RsiIndicator, SmaIndicator, StochasticIndicator);
StockChart.Inject(Trendlines, TmaIndicator, RangeTooltip, Tooltip, Crosshair, Export);
let stockChart: StockChart = new StockChart({
series: [
{
dataSource: chartData, type: 'Candle',
animation: { enable: true },
bearFillColor: '#2ecd71', bullFillColor: '#e74c3d',
}
],
});
stockChart.appendTo("#element");<!DOCTYPE html>
<html lang="en">
<head>
<title>EJ2 Animation</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Typescript UI Controls" />
<meta name="author" content="Syncfusion" />
<link href="./index.css" rel="stylesheet" />
<link href="http://cdn.syncfusion.com/ej2/material.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.38/system.js"></script>
<script src="./systemjs.config.js"></script>
</head>
<body>
<div id='loader'>Loading....</div>
<div id='container'>
<div id='element'></div>
</div>
</body>
</html>Handling No Data
When no data is available to render in the stock chart, the noDataTemplate property can be used to display a custom layout within the chart area. This layout may include a message indicating the absence of data, a relevant image, or a button to initiate data loading. Styled text, images, or interactive elements can be incorporated to maintain design consistency and improve user guidance. Once data becomes available, the chart automatically updates to display the appropriate visualization.
import { StockChart, IStockChartEventArgs } from '@syncfusion/ej2-charts';
import { chartData } from './datasource.ts';
import { DateTime, HiloOpenCloseSeries, CandleSeries } from '@syncfusion/ej2-charts';
import { AccumulationDistributionIndicator, AtrIndicator, BollingerBands, EmaIndicator, MomentumIndicator } from '@syncfusion/ej2-charts';
import { MacdIndicator, RsiIndicator, Trendlines, SmaIndicator, StochasticIndicator, Export } from '@syncfusion/ej2-charts';
import { TmaIndicator, RangeTooltip, Tooltip, Crosshair }
from '@syncfusion/ej2-charts';
import { Button } from '@syncfusion/ej2-buttons';
StockChart.Inject(DateTime, HiloOpenCloseSeries, CandleSeries);
StockChart.Inject(AccumulationDistributionIndicator, AtrIndicator, BollingerBands, EmaIndicator, MomentumIndicator);
StockChart.Inject(MacdIndicator, RsiIndicator, SmaIndicator, StochasticIndicator);
StockChart.Inject(Trendlines, TmaIndicator, RangeTooltip, Tooltip, Crosshair, Export);
let checked: boolean = false;
let stockChart: StockChart = new StockChart({
series: [
{
type: 'Candle',
animation: { enable: true },
bearFillColor: '#2ecd71', bullFillColor: '#e74c3d',
}
],
title: 'AAPL Stock Price',
noDataTemplate: `
<div id="noDataTemplateContainer" class="light-bg">
<div class="template-align">
<img src="no-data.png" alt="No Data"/>
</div>
<div class="template-align">
<p style="font-size: 15px; margin: 10px 0 0;"><strong>No data available to display.</strong></p>
</div>
<div class="template-align">
<button id="loadDataBtn" style="margin-top: 15px;"></button>
</div>
</div>
`,
loaded: (args: IStockChartEventArgs) => {
const btnElem: HTMLElement | null = document.getElementById('loadDataBtn');
if (btnElem) {
const loadBtn: Button = new Button({
content: 'Load Data',
iconCss: 'e-icons e-refresh',
cssClass: 'e-outline',
isPrimary: false
});
loadBtn.appendTo(btnElem);
loadBtn.element.onclick = (): void => {
checked = !checked;
if (checked) {
stockChart.series[0].dataSource = chartData;
}
stockChart.refresh();
};
}
}
});
stockChart.appendTo("#element");<!DOCTYPE html>
<html lang="en">
<head>
<title>EJ2 Animation</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Typescript UI Controls" />
<meta name="author" content="Syncfusion" />
<link href="./index.css" rel="stylesheet" />
<link href="http://cdn.syncfusion.com/ej2/material.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.38/system.js"></script>
<script src="./systemjs.config.js"></script>
<style>
#noDataTemplateContainer {
height: inherit;
width: inherit;
}
.load-data-btn {
border-radius: 4px !important;
text-transform: none !important;
}
.light-bg {
background-color: #fafafa;
color: #000000;
}
.template-align img {
max-width: 150px;
/* Adjust size as needed */
max-height: 150px;
margin-top: 55px;
}
.template-align {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
</style>
</head>
<body>
<div id='loader'>Loading....</div>
<div id='container'>
<div id='element'></div>
</div>
</body>
</html>Live Stock Chart
The Stock Chart can be updated in real time by pushing new and modified OHLC (open, high, low, close, volume) values into an existing candle series without re-rendering the entire chart. This is achieved with the help of the addPoint and setData methods exposed on a StockSeries instance.
-
setData(point, animationDuration)– replaces the current forming candle with the supplied point. Use this when you want to update the price of the candle that is currently being built. -
addPoint(point, animationDuration)– appends a brand-new candle to the end of the series. Use this when a new time bucket starts (for example, every new minute on a one-minute candle).
A typical real-time workflow uses both methods together:
- Start with a set of historical candles loaded through
dataSource. - On every tick, call
setDatato mutate the values of the in-progress candle. - When the candle interval elapses, call
addPointto begin a new candle whose open price matches the previous close.
In the following example, simulated one-minute OHLC data is generated locally and the chart is updated every 100 ms (using setInterval) to demonstrate live behavior. The forming candle is updated with setData, and a new candle is appended with addPoint every ten ticks.
import { StockChart, DateTime, CandleSeries, LastValueLabel } from '@syncfusion/ej2-charts';
StockChart.Inject(DateTime, CandleSeries, LastValueLabel);
interface StockData {
x: Date;
open: number;
high: number;
low: number;
close: number;
volume: number;
}
/**
* Simulated one-minute candle in milliseconds.
*/
const ONE_MINUTE_MS: number = 60 * 1000;
/**
* Tick frequency for updating the chart.
* In a real app this would be ~60000 (one minute per candle).
* For demo purposes it is 100 ms so changes are visible quickly.
*/
const UPDATE_INTERVAL_MS: number = 100;
/**
* After this many ticks, a brand-new one-minute candle is appended.
* With UPDATE_INTERVAL_MS at 100 ms this is roughly one new candle per second.
*/
const UPDATES_PER_CANDLE: number = 10;
/**
* setData is called every tick, so its animation is disabled.
*/
const UPDATE_ANIMATION_DURATION: number = 0;
/**
* Short animation when a new candle is appended.
*/
const ADD_ANIMATION_DURATION: number = 300;
/**
* Number of historical one-minute candles generated locally.
*/
const INITIAL_CANDLE_COUNT: number = 120;
/**
* Starting price used when generating the historical data.
*/
const INITIAL_PRICE: number = 375;
let stockChart: StockChart;
let stockData: StockData[] = [];
let updateTimer: number | null = null;
let updateIndex: number = 0;
/**
* Returns a random number between the given minimum and maximum.
*/
function randomBetween(minimum: number, maximum: number): number {
return minimum + Math.random() * (maximum - minimum);
}
/**
* Rounds the given value to two decimal places.
*/
function correctFloat(value: number): number {
return Number(value.toFixed(2));
}
/**
* Returns the timestamp (ms) representing the start of the current minute.
*/
function getCurrentMinute(): number {
return Math.floor(Date.now() / ONE_MINUTE_MS) * ONE_MINUTE_MS;
}
/**
* Builds a single historical OHLCV candle.
*/
function createHistoricalCandle(timestamp: number, openingPrice: number): StockData {
const movement: number = randomBetween(-1.5, 1.5);
const close: number = correctFloat(Math.max(1, openingPrice + movement));
const high: number = correctFloat(
Math.max(openingPrice, close) + randomBetween(0.05, 0.7)
);
const low: number = correctFloat(
Math.max(1, Math.min(openingPrice, close) - randomBetween(0.05, 0.7))
);
return {
x: new Date(timestamp),
open: correctFloat(openingPrice),
high: high,
low: low,
close: close,
volume: Math.round(randomBetween(1000, 5000))
};
}
/**
* Populates `stockData` with `INITIAL_CANDLE_COUNT` historical candles
* ending at the start of the current minute.
*/
function createInitialLocalData(): void {
const currentMinute: number = getCurrentMinute();
const startingTime: number =
currentMinute - (INITIAL_CANDLE_COUNT - 1) * ONE_MINUTE_MS;
let price: number = INITIAL_PRICE;
stockData = [];
for (let index: number = 0; index < INITIAL_CANDLE_COUNT; index++) {
const timestamp: number = startingTime + index * ONE_MINUTE_MS;
const candle: StockData = createHistoricalCandle(timestamp, price);
stockData.push(candle);
price = candle.close;
}
}
/**
* Returns the StockSeries wrapper that exposes the live
* setData / addPoint methods.
*/
function getCandleSeries(): any {
const chart: any = stockChart as any;
if (
chart &&
chart.series &&
chart.series.length &&
typeof chart.series[0].setData === 'function' &&
typeof chart.series[0].addPoint === 'function'
) {
return chart.series[0];
}
return null;
}
/**
* Builds an updated version of the currently forming candle.
* Open price and timestamp remain unchanged; close, high, low,
* and volume are re-randomized.
*/
function createUpdatedCandle(currentCandle: StockData): StockData {
const movement: number = correctFloat(randomBetween(-1.5, 1.5));
const newClose: number = correctFloat(Math.max(1, currentCandle.close + movement));
return {
x: currentCandle.x,
open: currentCandle.open,
high: correctFloat(Math.max(currentCandle.high, newClose)),
low: correctFloat(Math.min(currentCandle.low, newClose)),
close: newClose,
volume: currentCandle.volume + Math.round(randomBetween(10, 100))
};
}
/**
* Builds the next one-minute candle, opening at the previous close.
*/
function createNewCandle(previousCandle: StockData): StockData {
const openingPrice: number = previousCandle.close;
return {
x: new Date(previousCandle.x.getTime() + ONE_MINUTE_MS),
open: openingPrice,
high: openingPrice,
low: openingPrice,
close: openingPrice,
volume: Math.round(randomBetween(1000, 5000))
};
}
/**
* Calls series.setData() to replace the currently forming candle.
*/
function updateCurrentPoint(candle: StockData): void {
const series: any = getCandleSeries();
if (!series) {
return;
}
series.setData(candle, UPDATE_ANIMATION_DURATION);
}
/**
* Calls series.addPoint() to append a new one-minute candle.
*/
function addNewPoint(candle: StockData): void {
const series: any = getCandleSeries();
if (!series) {
return;
}
series.addPoint(candle, ADD_ANIMATION_DURATION);
}
/**
* Decides whether the current tick should update the forming candle
* or append a new one, then calls the appropriate series method.
*/
function processDynamicUpdate(): void {
if (!stockData.length) {
return;
}
const lastIndex: number = stockData.length - 1;
const currentCandle: StockData = stockData[lastIndex];
const shouldAddNewCandle: boolean =
updateIndex > 0 && updateIndex % UPDATES_PER_CANDLE === 0;
if (shouldAddNewCandle) {
const newCandle: StockData = createNewCandle(currentCandle);
stockData.push(newCandle);
addNewPoint(newCandle);
} else {
const updatedCandle: StockData = createUpdatedCandle(currentCandle);
stockData[lastIndex] = updatedCandle;
updateCurrentPoint(updatedCandle);
}
updateIndex++;
}
/**
* Starts the simulated live update loop.
*/
function startDynamicUpdates(): void {
if (updateTimer !== null) {
return;
}
updateTimer = window.setInterval(processDynamicUpdate, UPDATE_INTERVAL_MS);
}
/**
* Stops the simulated live update loop.
*/
function stopDynamicUpdates(): void {
if (updateTimer !== null) {
window.clearInterval(updateTimer);
updateTimer = null;
}
}
createInitialLocalData();
stockChart = new StockChart({
width: '1100px',
height: '455px',
title: 'Live Stock Data',
theme: 'Material',
chartArea: {
border: { width: 0 }
},
primaryXAxis: {
valueType: 'DateTime',
labelFormat: 'HH:mm'
},
primaryYAxis: {
labelPosition: 'Outside'
},
series: [
{
dataSource: stockData,
type: 'Candle',
xName: 'x',
open: 'open',
high: 'high',
low: 'low',
close: 'close',
volume: 'volume',
lastValueLabel: {
enable: true,
background: '#e74c3d',
dashArray: '3,2',
lineWidth: 1,
font: {
color: '#ffffff',
size: '11px'
}
}
}
],
seriesType: [],
indicatorType: [],
trendlineType: [],
periods: [
{ text: '1H', interval: 1, intervalType: 'Hours', selected: true },
{ text: 'All' }
],
enableCustomRange: false,
loaded: (): void => {
startDynamicUpdates();
}
});
stockChart.appendTo('#container');
/**
* Clears the update timer when the sample page is unloaded.
*/
window.addEventListener('beforeunload', (): void => {
stopDynamicUpdates();
});<!DOCTYPE html>
<html lang="en">
<head>
<title>EJ2 Live Stock Chart</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Typescript UI Controls" />
<meta name="author" content="Syncfusion" />
<link href="./index.css" rel="stylesheet" />
<link href="http://cdn.syncfusion.com/ej2/material.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.38/system.js"></script>
<script src="systemjs.config.js"></script>
</head>
<body>
<div id='loader'>Loading....</div>
<div class="control-section" style="padding:5%">
<div id="container" role="img"
aria-label="Stock Chart with live data updates using addPoint and setData"></div>
</div>
</body>
</html>