Create a heatmap chart showing historical orderbook levels
drawExample.ts
index.tsx
theme.ts
createCandlestickChart.tsx
1import {
2 SciChartSurface,
3 NumericAxis,
4 ENumericFormat,
5 ZoomPanModifier,
6 ZoomExtentsModifier,
7 MouseWheelZoomModifier,
8 NumberRange,
9 OhlcDataSeries,
10 FastCandlestickRenderableSeries,
11 SciChartJsNavyTheme,
12 DateTimeNumericAxis,
13 CursorModifier,
14 CursorTooltipSvgAnnotation,
15 EDataSeriesType,
16 ESeriesType,
17 FastMountainRenderableSeries,
18 GradientParams,
19 IRenderableSeries,
20 OhlcSeriesInfo,
21 Point,
22 SeriesInfo,
23 HeatmapColorMap,
24 UniformHeatmapRenderableSeries,
25 UniformHeatmapDataSeries,
26 EXyDirection,
27} from "scichart";
28
29import { appTheme } from "../../../theme";
30
31// Data file paths (files are copied via webpack.config.js)
32const ohlcFilePath = "LTCUSDT_OHLC.csv";
33const orderbookLevels = "orderbook_levels.csv";
34
35const baseUrl =
36 typeof window !== "undefined" &&
37 !window.location.hostname.includes("scichart.com") &&
38 !window.location.hostname.includes("localhost")
39 ? "https://www.scichart.com/demo"
40 : "";
41
42/** OHLCV candlestick data structure */
43type TCandleData = {
44 xValues: number[];
45 openValues: number[];
46 highValues: number[];
47 lowValues: number[];
48 closeValues: number[];
49 volumeValues: number[];
50};
51
52/**
53 * Loads OHLCV candlestick data from CSV file
54 * @returns Promise resolving to parsed candle data arrays
55 */
56async function loadCandleData(): Promise<TCandleData> {
57 const xValues: number[] = [];
58 const openValues: number[] = [];
59 const highValues: number[] = [];
60 const lowValues: number[] = [];
61 const closeValues: number[] = [];
62 const volumeValues: number[] = [];
63
64 try {
65 const filepath = baseUrl + ohlcFilePath;
66 const response = await fetch(filepath);
67
68 if (!response.ok) {
69 throw new Error(`HTTP error! status: ${response.status}`);
70 }
71
72 const csvText = await response.text();
73 const lines = csvText.split("\n");
74
75 // Parse each data row (skip header at index 0)
76 for (let i = 1; i < lines.length; i++) {
77 const line = lines[i].trim();
78 if (!line) continue;
79
80 const rowData = line.split(",");
81
82 if (rowData.length >= 6) {
83 const priceBar = {
84 date: Number.parseInt(rowData[0]),
85 open: Number.parseFloat(rowData[1]),
86 high: Number.parseFloat(rowData[2]),
87 low: Number.parseFloat(rowData[3]),
88 close: Number.parseFloat(rowData[4]),
89 volume: Number.parseFloat(rowData[5]),
90 };
91
92 // Only add valid numeric data
93 if (
94 !isNaN(priceBar.date) &&
95 !isNaN(priceBar.open) &&
96 !isNaN(priceBar.high) &&
97 !isNaN(priceBar.low) &&
98 !isNaN(priceBar.close) &&
99 !isNaN(priceBar.volume)
100 ) {
101 xValues.push(priceBar.date);
102 openValues.push(priceBar.open);
103 highValues.push(priceBar.high);
104 lowValues.push(priceBar.low);
105 closeValues.push(priceBar.close);
106 volumeValues.push(priceBar.volume);
107 }
108 }
109 }
110
111 return {
112 xValues,
113 openValues,
114 highValues,
115 lowValues,
116 closeValues,
117 volumeValues,
118 };
119 } catch (error) {
120 console.error("Error loading candle data:", error);
121 throw error;
122 }
123}
124
125/** Parsed order book heatmap data structure */
126type TParsedHeatmapData = {
127 /** 2D array of order values (Z-axis intensity) */
128 zValues: number[][];
129 /** Unix timestamps for X-axis cell positions */
130 xCellOffsets: number[];
131 /** Price levels for Y-axis cell positions */
132 yCellOffsets: number[];
133 /** Minimum non-zero value in the heatmap */
134 minValue: number;
135 /** Maximum value in the heatmap */
136 maxValue: number;
137};
138
139/**
140 * Loads order book depth heatmap data from CSV file
141 * CSV format: First row contains timestamps, subsequent rows contain price level and order values
142 * @returns Promise resolving to parsed heatmap data with Z-values and axis offsets
143 */
144async function loadHeatmapData(): Promise<TParsedHeatmapData> {
145 const zValues: number[][] = [];
146 let xCellOffsets: number[] = [];
147 const yCellOffsets: number[] = [];
148 let minValue = Infinity;
149 let maxValue = -Infinity;
150
151 try {
152 const dataFile = baseUrl + orderbookLevels;
153 const response = await fetch(dataFile);
154 const csvText = await response.text();
155 const lines = csvText.split("\n");
156
157 for (let i = 0; i < lines.length; i++) {
158 const line = lines[i].trim();
159 if (!line) continue;
160
161 const rowData = line.split(",");
162
163 if (i === 0) {
164 // Header row contains Unix timestamps for each column
165 const [_, ...cellOffsets] = rowData;
166 xCellOffsets = cellOffsets.map((timestampString: string) => {
167 const timestamp = Number.parseInt(timestampString);
168 return timestamp || 0;
169 });
170 } else {
171 // Data rows: first column is price level, remaining columns are order values
172 const [price, ...zValuesRow] = rowData;
173
174 if (!Number.isNaN(Number.parseInt(price))) {
175 const rowValues = zValuesRow.map((val: string) => {
176 const numVal = Number.parseFloat(val) || 0;
177 // Track min/max for color map scaling (exclude zeros)
178 if (numVal > 0) {
179 minValue = Math.min(minValue, numVal);
180 maxValue = Math.max(maxValue, numVal);
181 }
182 return numVal;
183 });
184
185 zValues.push(rowValues);
186 yCellOffsets.push(Number.parseFloat(price));
187 }
188 }
189 }
190
191 // Default range if no valid values found
192 if (minValue === Infinity) {
193 minValue = 0;
194 maxValue = 1;
195 }
196
197 return { zValues, xCellOffsets, yCellOffsets, minValue, maxValue };
198 } catch (error) {
199 console.error("Error loading heatmap data:", error);
200 throw error;
201 }
202}
203
204/**
205 * Creates an Order Book Heatmap chart with candlestick overlay
206 * Demonstrates combining UniformHeatmapRenderableSeries with FastCandlestickRenderableSeries
207 * to visualize order book depth alongside price action
208 *
209 * @param rootElement - HTML element ID or element to render the chart into
210 * @returns Promise resolving to the chart surface and candlestick series
211 */
212export const drawExample = async (rootElement: string | HTMLDivElement) => {
213 const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
214 theme: new SciChartJsNavyTheme(),
215 });
216
217 // X-Axis: DateTimeNumericAxis for continuous time data
218 // Note: For stocks/forex with market hours, use DiscontinuousDateAxis to collapse weekend gaps
219 const xAxis = new DateTimeNumericAxis(wasmContext, {
220 cursorLabelFormat: ENumericFormat.Date_HHMMSS,
221 });
222 sciChartSurface.xAxes.add(xAxis);
223
224 // Y-Axis: Price axis with currency formatting
225 const priceAxis = new NumericAxis(wasmContext, {
226 labelFormat: ENumericFormat.Decimal,
227 labelPrecision: 2,
228 labelPrefix: "$",
229 });
230 sciChartSurface.yAxes.add(priceAxis);
231
232 // Load data from CSV files
233 const { xValues, openValues, highValues, lowValues, closeValues, volumeValues } = await loadCandleData();
234 const { zValues, xCellOffsets, yCellOffsets, minValue, maxValue } = await loadHeatmapData();
235
236 // Heatmap color gradient: dark background -> white (low values) -> red (high values)
237 const gradientStops = [
238 { offset: 0, color: appTheme.DarkIndigo },
239 { offset: 0.03, color: appTheme.ForegroundColor },
240 { offset: 0.4, color: appTheme.VividRed },
241 ];
242
243 // Color map scales Z-values to gradient colors
244 const colorMap = new HeatmapColorMap({
245 minimum: minValue,
246 maximum: maxValue,
247 gradientStops,
248 });
249
250 // Calculate average X-axis step size for uniform heatmap spacing
251 let totalStep = 0;
252 let stepCount = 0;
253 for (let i = 1; i < xCellOffsets.length; i++) {
254 totalStep += xCellOffsets[i] - xCellOffsets[i - 1];
255 stepCount++;
256 }
257 const averageXStep = stepCount > 0 ? totalStep / stepCount : xCellOffsets[1] - xCellOffsets[0];
258
259 // Calculate average Y-axis step size for uniform heatmap spacing
260 let totalYStep = 0;
261 let yStepCount = 0;
262 for (let i = 1; i < yCellOffsets.length; i++) {
263 totalYStep += yCellOffsets[i] - yCellOffsets[i - 1];
264 yStepCount++;
265 }
266 const averageYStep = yStepCount > 0 ? totalYStep / yStepCount : yCellOffsets[1] - yCellOffsets[0];
267
268 // Create heatmap data series with uniform cell spacing
269 const heatmapDataSeries = new UniformHeatmapDataSeries(wasmContext, {
270 xStart: xCellOffsets[0],
271 xStep: averageXStep,
272 yStart: yCellOffsets[0],
273 yStep: averageYStep,
274 zValues,
275 dataSeriesName: "Order Value",
276 });
277
278 // Calculate data ranges for axis configuration
279 const candleXRange = [Math.min(...xValues), Math.max(...xValues)];
280 const heatmapXRange = [Math.min(...xCellOffsets), Math.max(...xCellOffsets)];
281
282 // Create heatmap series with semi-transparency to show candlesticks through it
283 const heatmapSeries = new UniformHeatmapRenderableSeries(wasmContext, {
284 opacity: 0.4,
285 dataSeries: heatmapDataSeries,
286 colorMap,
287 stroke: appTheme.PaleSkyBlue,
288 });
289 // Disable texture filtering for crisp cell boundaries
290 heatmapSeries.useLinearTextureFiltering = false;
291
292 sciChartSurface.renderableSeries.add(heatmapSeries);
293
294 // Set Y-axis to show full price range from heatmap data
295 const heatmapYRange = [Math.min(...yCellOffsets), Math.max(...yCellOffsets)];
296 priceAxis.visibleRange = new NumberRange(heatmapYRange[0], heatmapYRange[1]);
297
298 // Set X-axis to show overlapping time range between candle and heatmap data
299 const overlapStart = Math.max(candleXRange[0], heatmapXRange[0]);
300 const overlapEnd = Math.min(candleXRange[1], heatmapXRange[1]);
301
302 if (overlapStart < overlapEnd) {
303 xAxis.visibleRange = new NumberRange(overlapStart, overlapEnd);
304 } else {
305 // No overlap - default to heatmap range
306 xAxis.visibleRange = new NumberRange(heatmapXRange[0], heatmapXRange[1]);
307 }
308
309 // Create candlestick series with OHLC data
310 const candleDataSeries = new OhlcDataSeries(wasmContext, {
311 xValues,
312 openValues,
313 highValues,
314 lowValues,
315 closeValues,
316 dataSeriesName: "LTC/USDT",
317 });
318
319 const candlestickSeries = new FastCandlestickRenderableSeries(wasmContext, {
320 dataSeries: candleDataSeries,
321 stroke: appTheme.ForegroundColor,
322 strokeThickness: 2,
323 brushUp: appTheme.VividGreen + "AA",
324 brushDown: appTheme.MutedRed + "AA",
325 strokeUp: appTheme.VividGreen,
326 strokeDown: appTheme.MutedRed,
327 dataPointWidth: 0.8,
328 isVisible: true,
329 opacity: 1,
330 });
331
332 sciChartSurface.renderableSeries.add(candlestickSeries);
333 console.log("Added candlestick series with", candleDataSeries.count(), "data points");
334
335 // Add chart interaction modifiers
336 sciChartSurface.chartModifiers.add(
337 new ZoomExtentsModifier(),
338 new ZoomPanModifier({
339 enableZoom: true,
340 horizontalGrowFactor: 0.005,
341 verticalGrowFactor: 0,
342 xyDirection: EXyDirection.XDirection,
343 }),
344 new MouseWheelZoomModifier({ xyDirection: EXyDirection.XDirection }),
345 new CursorModifier({
346 crosshairStroke: appTheme.PaleOrange + 55,
347 axisLabelFill: appTheme.PaleOrange + 55,
348 tooltipLegendTemplate: getTooltipLegendTemplate,
349 })
350 );
351
352 return { sciChartSurface, candlestickSeries };
353};
354
355/**
356 * Transforms series for the SciChart overview/navigator component
357 * Converts candlestick series to mountain series for cleaner overview display
358 * @param defaultSeries - The original renderable series
359 * @returns Transformed series for overview, or undefined to hide
360 */
361const getOverviewSeries = (defaultSeries: IRenderableSeries) => {
362 if (defaultSeries.type === ESeriesType.CandlestickSeries) {
363 // Display candlestick data as a mountain series in the overview for cleaner visualization
364 return new FastMountainRenderableSeries(defaultSeries.parentSurface.webAssemblyContext2D, {
365 dataSeries: defaultSeries.dataSeries,
366 fillLinearGradient: new GradientParams(new Point(0, 0), new Point(0, 1), [
367 { color: appTheme.VividSkyBlue + "77", offset: 0 },
368 { color: "Transparent", offset: 1 },
369 ]),
370 stroke: appTheme.VividSkyBlue,
371 });
372 }
373 // Hide other series (e.g., heatmap) from overview
374 return undefined;
375};
376
377/** Configuration for SciChart overview/navigator component */
378export const sciChartOverview = {
379 theme: appTheme.SciChartJsTheme,
380 transformRenderableSeries: getOverviewSeries,
381};
382
383/**
384 * Custom tooltip template for CursorModifier
385 * Displays OHLC values for candlestick series and order values for heatmap
386 * @param seriesInfos - Array of series info objects for series under cursor
387 * @param svgAnnotation - The tooltip annotation instance
388 * @returns SVG string for tooltip content
389 */
390const getTooltipLegendTemplate = (seriesInfos: SeriesInfo[], svgAnnotation: CursorTooltipSvgAnnotation) => {
391 let outputSvgString = "";
392
393 seriesInfos.forEach((seriesInfo, index) => {
394 const y = 20 + index * 20;
395 const textColor = seriesInfo.stroke;
396 let legendText = seriesInfo.formattedYValue;
397
398 // Format OHLC data for candlestick series
399 if (seriesInfo.dataSeriesType === EDataSeriesType.Ohlc) {
400 const o = seriesInfo as OhlcSeriesInfo;
401 legendText = `Open=${o.formattedOpenValue} High=${o.formattedHighValue} Low=${o.formattedLowValue} Close=${o.formattedCloseValue}`;
402 }
403 // Format heatmap data showing price level and order count
404 else if (seriesInfo.seriesName === "Order Value") {
405 legendText = `${seriesInfo.formattedYValue} Orders: ${seriesInfo.hitTestInfo.zValue}`;
406 }
407
408 outputSvgString += `<text x="8" y="${y}" font-size="13" font-family="Verdana" fill="${textColor}">
409 ${seriesInfo.seriesName}: ${legendText}
410 </text>`;
411 });
412
413 return `<svg width="100%" height="100%">
414 <g transform=translate(5,5)>
415 ${
416 outputSvgString ? `<rect width="480px" height="50px" fill="#000000" opacity="0.4" rx="5" />` : ``
417 }
418 ${outputSvgString}
419 <g>
420 </svg>`;
421};
422This example demonstrates how to create an Order Book Heatmap visualization using SciChart.js, combining a UniformHeatmapRenderableSeries with a FastCandlestickRenderableSeries to display historical orderbook depth levels alongside price candles.
An order book heatmap visualizes the distribution of buy and sell orders at different price levels over time. The intensity of color represents the volume of orders at each price point, helping traders identify:
The heatmap uses a custom color gradient that transitions from dark (low volume) through white to red (high volume), making it easy to spot areas of high liquidity. The candlestick series is overlaid with semi-transparent colors to ensure both datasets remain visible.

Discover how to create a JavaScript Candlestick Chart or Stock Chart using SciChart.js. For high Performance JavaScript Charts, get your free demo now.

Easily create JavaScript OHLC Chart or Stock Chart using feature-rich SciChart.js chart library. Supports custom colors. Get your free trial now.

Create a JavaScript Realtime Ticking Candlestick / Stock Chart with live ticking and updating, using the high performance SciChart.js chart library. Get free demo now.

Create a JavaScript Multi-Pane Candlestick / Stock Chart with indicator panels, synchronized zooming, panning and cursors. Get your free trial of SciChart.js now.

Demonstrating the capability of SciChart.js to create a composite 2D & 3D Chart application. An example like this could be used to visualize Tenor curves in a financial setting, or other 2D/3D data combined on a single screen.

Create a JavaScript Multi-Pane Candlestick / Stock Chart with indicator panels, synchronized zooming, panning and cursors. Get your free trial of SciChart.js now.

Create a JavaScript Depth Chart, using the high performance SciChart.js chart library. Get free demo now.

Demonstrates how to place Buy/Sell arrow markers on a JavaScript Stock Chart using SciChart.js - Annotations API

This demo shows you how to create a <strong>{frameworkName} User Annotated Stock Chart</strong> using SciChart.js. Custom modifiers allow you to add lines and markers, then use the built in serialisation functions to save and reload the chart, including the data and all your custom annotations.