JavaScript Orderbook Heatmap

Create a heatmap chart showing historical orderbook levels

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.tsx

theme.ts

createCandlestickChart.ts

Copy to clipboard
Minimise
Fullscreen
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};
502

Order Book Heatmap Chart

This 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.

What is an Order Book Heatmap?

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:

  • Support and resistance levels - Areas with high order concentration
  • Liquidity zones - Where large orders are clustered
  • Price walls - Significant order accumulations that may affect price movement

Key Features

  • UniformHeatmapDataSeries - Efficiently stores 2D grid data with uniform X and Y spacing
  • HeatmapColorMap - Configurable gradient color mapping from order volume to visual intensity
  • Candlestick overlay - Price action displayed on top of the heatmap for context
  • Custom tooltips - CursorModifier with custom template showing OHLC data and order values
  • Interactive navigation - ZoomPanModifier, MouseWheelZoomModifier, and ZoomExtentsModifier for exploring the data

Implementation Highlights

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.

javascript Chart Examples & Demos

See Also: Financial Charts (13 Demos)

JavaScript Candlestick Chart | Online JavaScript Chart Examples

JavaScript Candlestick Chart

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

JavaScript OHLC Chart | Javascript Charts | SciChart.js Demo

JavaScript OHLC Chart

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

JavaScript Realtime Ticking Stock Chart | SciChart.js

JavaScript Realtime Ticking Stock Charts

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.

JavaScript Multi-Pane Stock Chart using Subcharts | View JavaScript Charts

JavaScript Multi-Pane Stock Charts using Subcharts

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

Tenor Curves Demo | Javascript Charts | SciChart.js Demo

Tenor Curves Demo

Demonstrating the capability of SciChart.js to create a composite 2D &amp; 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.

JavaScript Multi-Pane Stock Chart | View JavaScript Charts

JavaScript Multi-Pane Stock Charts using Sync Multi-Chart

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

Financial Data Filters | Javascript Charts | SciChart.js Demo

Financial Data Filters

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

JavaScript Market Depth Chart | Javascript Charts | SciChart.js

JavaScript Market Depth Chart

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

JavaScript Chart Hoverable Buy Sell Marker Annotations

JavaScript Chart Hoverable Buy Sell Marker Annotations

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

JavaScript User Annotated Stock Chart | SciChart.js Demo

JavaScript User Annotated Stock Chart

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.

JavaScript Trading Drawing Tools | Javascript Charts | SciChart.js

JavaScript Trading Drawing Tools

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.

JavaScript Custom Paired Dashed Polyline Annotation

JavaScript Custom Paired Dashed Polyline Annotation

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

JavaScript Freehand Drawing Tools | SciChart.js Demo

JavaScript Freehand Drawing Tools

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

SciChart Ltd, 16 Beaufort Court, Admirals Way, Docklands, London, E14 9XL.