A candlestick chart fed by a simulated live feed: a new price every second, a new candle every minute. The label on the price axis follows the latest price, green while it is above the minute’s open and red below.
Live prices change by the second, and the chart keeps up by changing only its newest candle: each tick moves its close and stretches its high or low, and the first tick of a new minute opens the next candle. Here the ticks are random; in your app they would come from a WebSocket or a call to your server.
Built with amCharts 5, a JavaScript library. The code includes your theme and renderer (canvas or SVG), but not the changes you make with the controls.
npm install @amcharts/amcharts5. See Getting started.
The same chart as a JSON config. Stock Chart reads and writes this format, the library's JSON parser turns it into a chart, and the <am5-chart> element puts it on a page with no code."type" is the class to create and "settings" what you would pass to its new() in code; "properties" fill in its parts, lists such as series and axes or templates such as columns.template; "refs" holds objects used in more than one place ("#series-0" stands for the one named series-0); a string starting with "@" takes a value from another object, such as "@series.get('fill')".
A complete page. Save it as an .html file and open it in a browser.
Paste this into an AI coding assistant to build the chart from scratch. More in amCharts & AI.
// Create root element
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/getting-started/#Root_element
var root = am5.Root.new("chartdiv");
const myTheme = am5.Theme.new(root); // a theme of our own, for the rule below
// hide the minor grid lines in the scrollbar's small chart
myTheme.rule("Grid", ["scrollbar", "minor"]).setAll({
visible:false
});
root.setThemes([
am5themes_Animated.new(root),
myTheme,
am5themes_Responsive.new(root)
]);
// Create a stock chart
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/stock/#Instantiating_the_chart
var stockChart = root.container.children.push(
am5stock.StockChart.new(root, {
paddingRight: 0 // no gap at the right edge
})
);
// Set global number format
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/concepts/formatters/formatting-numbers/
root.numberFormatter.set("numberFormat", "#,###.00"); // thousands separators, always two decimals
// Create a main stock panel (chart)
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/stock/#Adding_panels
var mainPanel = stockChart.panels.push(
am5stock.StockPanel.new(root, {
wheelY: "zoomX", // the mouse wheel zooms in on the time
panX: true, // drag the plot sideways to pan
panY: true // and up and down
})
);
// Create value axis
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/xy-chart/axes/
var valueAxis = mainPanel.yAxes.push(
am5xy.ValueAxis.new(root, {
renderer: am5xy.AxisRendererY.new(root, {
pan: "zoom" // drag along the value labels to zoom the axis
}),
extraMin: 0.1, // adds some space for the main series
tooltip: am5.Tooltip.new(root, {}), // a price label follows the cursor along the axis
numberFormat: "#,###.00", // two decimals on the labels...
extraTooltipPrecision: 2 // ...and two more in the axis tooltip
})
);
var dateAxis = mainPanel.xAxes.push(
am5xy.GaplessDateAxis.new(root, { // no empty space for minutes without data
baseInterval: { // one candle per minute
timeUnit: "minute",
count: 1
},
renderer: am5xy.AxisRendererX.new(root, {
pan: "zoom", // drag along the time labels to zoom
minorGridEnabled: true // fainter grid lines between the labeled ones
}),
tooltip: am5.Tooltip.new(root, {}) // a time label follows the cursor along the axis
})
);
// add range which will show current value
var currentValueDataItem = valueAxis.createAxisRange(valueAxis.makeDataItem({ value: 0 }));
var currentLabel = currentValueDataItem.get("label");
if (currentLabel) {
currentLabel.setAll({
fill: am5.color(0xffffff), // white text on a box that turns red or green with the price
background: am5.Rectangle.new(root, { fill: am5.color(0x000000) })
})
}
var currentGrid = currentValueDataItem.get("grid");
if (currentGrid) {
currentGrid.setAll({ strokeOpacity: 0.5, strokeDasharray: [2, 5] }); // faint, 2px dashes, 5px gaps
}
// Add series
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/xy-chart/series/
var valueSeries = mainPanel.series.push(
am5xy.CandlestickSeries.new(root, {
// draws the candles straight onto the canvas, faster (no rounded corners or column events)
turboMode: true,
name: "AMCH",
clustered: false, // not placed side by side with other column series
valueXField: "Date",
valueYField: "Close",
highValueYField: "High",
lowValueYField: "Low",
openValueYField: "Open",
calculateAggregates: true, // works out change, high, low and such for the stock tools
xAxis: dateAxis,
yAxis: valueAxis,
legendValueText: // the legend shows the hovered candle's prices
"open: [bold]{openValueY}[/] high: [bold]{highValueY}[/] low: [bold]{lowValueY}[/] close: [bold]{valueY}[/]",
legendRangeValueText: "" // and no values while the cursor is off the chart
})
);
// Set main value series
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/stock/#Setting_main_series
stockChart.set("stockSeries", valueSeries);
// Add a stock legend
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/stock/stock-legend/
var valueLegend = mainPanel.plotContainer.children.push(
am5stock.StockLegend.new(root, {
stockChart: stockChart
})
);
// Set main series
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/stock/#Setting_main_series
valueLegend.data.setAll([valueSeries]);
// Add cursor(s)
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/xy-chart/cursor/
mainPanel.set(
"cursor",
am5xy.XYCursor.new(root, {
yAxis: valueAxis, // the cursor works with these axes...
xAxis: dateAxis, // ...and snaps to the minutes
snapToSeries: [valueSeries], // the cursor snaps to the candles...
snapToSeriesBy: "y!" // ...the horizontal line to the close of the candle under it
})
);
// Add scrollbar
// -------------------------------------------------------------------------------
// https://www.amcharts.com/docs/v5/charts/xy-chart/scrollbars/
var scrollbar = mainPanel.set(
"scrollbarX",
am5xy.XYChartScrollbar.new(root, {
orientation: "horizontal", // a strip under the chart...
height: 50 // ...50px tall, with a small chart of the price in it
})
);
// move the scrollbar out of the panel, to the bottom of the stock chart
stockChart.toolsContainer.children.push(scrollbar);
// the small chart inside the scrollbar gets its own axes and series
var sbDateAxis = scrollbar.chart.xAxes.push(
am5xy.GaplessDateAxis.new(root, {
baseInterval: {
timeUnit: "minute", // one point per minute
count: 1
},
renderer: am5xy.AxisRendererX.new(root, {
minorGridEnabled: true // minor grid lines, hidden by the theme rule at the top
})
})
);
var sbValueAxis = scrollbar.chart.yAxes.push(
am5xy.ValueAxis.new(root, {
renderer: am5xy.AxisRendererY.new(root, {})
})
);
var sbSeries = scrollbar.chart.series.push(
am5xy.LineSeries.new(root, {
valueYField: "Close",
valueXField: "Date",
xAxis: sbDateAxis,
yAxis: sbValueAxis
})
);
sbSeries.fills.template.setAll({
visible: true, // fill the area under the line...
fillOpacity: 0.3 // ...30% opaque
});
// Data generator
var firstDate = new Date(); // the data ends now
var lastDate;
var value = 1200; // the price starts around 1200
// data
function generateChartData() {
var chartData = [];
// 50 candles, one per minute going back from now (unshift keeps them in time order)
for (var i = 0; i < 50; i++) {
var newDate = new Date(firstDate);
newDate.setMinutes(newDate.getMinutes() - i);
value += Math.round((Math.random() < 0.49 ? 1 : -1) * Math.random() * 10); // a random step of up to 10
var open = value + Math.round(Math.random() * 16 - 8); // open within 8 of the close
var low = Math.min(value, open) - Math.round(Math.random() * 5); // low a little under both
var high = Math.max(value, open) + Math.round(Math.random() * 5); // high a little over both
chartData.unshift({
Date: newDate.getTime(),
Close: value,
Open: open,
Low: low,
High: high
});
lastDate = newDate;
}
return chartData;
}
var data = generateChartData();
// set data to all series
valueSeries.data.setAll(data);
sbSeries.data.setAll(data);
// update data
var previousDate;
// every second: a new price, added to the current minute's candle or starting a new one
setInterval(function () {
var valueSeries = stockChart.get("stockSeries"); // the stock chart's main series
var date = Date.now(); // time now
var lastDataObject = valueSeries.data.getIndex(valueSeries.data.length - 1); // the latest candle
if (lastDataObject) {
var previousDate = lastDataObject.Date;
var previousValue = lastDataObject.Close;
// the price moves randomly by up to 2 from the last close, rounded to 2 decimals
value = am5.math.round(previousValue + (Math.random() < 0.5 ? 1 : -1) * Math.random() * 2, 2);
var high = lastDataObject.High;
var low = lastDataObject.Low;
var open = lastDataObject.Open;
// a new minute starts a new candle, otherwise the last candle is updated
if (am5.time.checkChange(date, previousDate, "minute")) {
open = value;
high = value;
low = value;
var dObj1 = {
Date: date,
Close: value,
Open: value,
Low: value,
High: value
};
valueSeries.data.push(dObj1);
sbSeries.data.push(dObj1);
previousDate = date;
} else {
if (value > high) { // a new high for this minute
high = value;
}
if (value < low) { // a new low for this minute
low = value;
}
var dObj2 = {
Date: date,
Close: value,
Open: open,
Low: low,
High: high
};
valueSeries.data.setIndex(valueSeries.data.length - 1, dObj2); // replace the last candle
sbSeries.data.setIndex(sbSeries.data.length - 1, dObj2);
}
// update current value
if (currentLabel) {
// the price line glides to the new price, and its label shows it
currentValueDataItem.animate({ key: "value", to: value, duration: 500, easing: am5.ease.out(am5.ease.cubic) });
currentLabel.set("text", stockChart.getNumberFormatter().format(value));
var bg = currentLabel.get("background");
if (bg) {
if (value < open) { // below the candle's open: red box, otherwise green
bg.set("fill", root.interfaceColors.get("negative"));
}
else {
bg.set("fill", root.interfaceColors.get("positive"));
}
}
}
}
}, 1000);