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

Shows one seeded OHLC data set toggled between no filter, Heikin-Ashi, Renko, and Point & Figure views.

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.

Create an interactive JavaScript trading charts for technical analysis. Trading Drawing Tools Demo, which shows how to use Polylines, Extended Lines, Rays, Channels, Pitchforks, Pitchfans, Fibonnaci Retracements, Measure, Stop Loss and Take Profit chart drawing tools for Technical Analysis.

Create a custom JavaScript stock chart annotation by extending SciChart.js PolyLineAnnotation with paired dashed connectors.

An example of using JavaScript FreehandDrawingModifier for arbitrary drawing on trading and financial charts. Can be used for drawing trends, arrow, markers, text, etc.