How to create a Volume Profile in a JavaScript Financial Chart
Tags: javascript, javascriptlibraries, dataviz, charts
Published: 2023-12-19 18:53:03

In financial stock charts, a **candlestick chart** is used to visualise open, high, low, close prices of an asset over a specific timeframe. The volume (amount traded) in each specific candle is significant for traders. Sometimes areas of high or low **volume** can act as support/resistance (difficult areas for the price to pass). Visualising the price levels associated with high volume can be done with a **Volume Profile chart**.

This post covers how to create the following elements on a JavaScript Chart:

- Candles showing price data in open, high, low, close format from an exchange
- Volume bars coloured red/green according to buy or sell
- A rotated** volume profile histogram** with calculated volume distribution docked to the right of the chart

For the purpose of the blog, we will be using SciChart.js - a commercial [JavaScript Chart Library](https://www.scichart.com/javascript-chart-features/) with a free community edition that can be used in non-commercial, educational or personal projects.

At the end of the blog post you will be able to create this in JavaScript. Full source-code is shared at the end of the post.

![JavaScript Volume Profile Chart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4yyae5njedr65y5o5x98.png)

# Initializing the Chart Layout

First of all we need a layout which meets the specification. 

![Rotated Chart docked inside a Chart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2unz0kd4ij95n86f1yqo.png)

In SciChart.js this can be achieved by creating a chart with an X,Y axis, but then adding a second X,Y axis with `xAxis.axisAlignment = EAxisAlignment.Left` and `yAxis.axisAlignment = EAxisAlignment.Top` respectively. An axis may then be hidden by setting `axis.isVisible = false`.

When the alignments of axis are switched in SciChart.js, you create a **vertical chart**, and with this library it's possible to have two sets of axis on the same chart which are transposed. 

Here’s how to setup a transposed axis on the same chart as a normal X,Y axis in JavaScript using SciChart.js. This technique allows you to rotate a specific series on the chart.

```javascript
const { 
  SciChartSurface,
  NumericAxis,
  SciChartJSLightTheme,
  EAxisAlignment,
  Thickness,
  FastLineRenderableSeries,
  XyDataSeries,
  NumberRange,
  ZoomPanModifier,
  MouseWheelZoomModifier,
  ZoomExtentsModifier
} = SciChart;

async function initSciChart(divElementId) {
  const { sciChartSurface, wasmContext } = await SciChartSurface.create(divElementId, { 
    title: "Custom layout JS Chart with Transposed Axis", 
    titleStyle: { fontSize: 28, fontWeight: "bold" },
    theme: new SciChartJSLightTheme()
  });
  
  // Add the primary x,y axis
  sciChartSurface.xAxes.add(new NumericAxis(wasmContext, {
    axisTitle: "X Axis",
    backgroundColor: "#0066ff33"
  }));
  sciChartSurface.yAxes.add(new NumericAxis(wasmContext, {
    axisTitle: "Y Axis",
    backgroundColor: "#0066ff33"
  }));
  
  // Add the secondary y,x axis transposed
  sciChartSurface.xAxes.add(new NumericAxis(wasmContext, {
    id: "SecondaryXAxis",
    axisAlignment: EAxisAlignment.Right,    
    axisTitle: "X Axis Transposed",
    backgroundColor: "#ff660033"
  }));
  sciChartSurface.yAxes.add(new NumericAxis(wasmContext, {
    id: "SecondaryYAxis",
    axisAlignment: EAxisAlignment.Bottom,
    axisTitle: "Y Axis Transposed",
    backgroundColor: "#ff660033",
    // growBy sets padding and yMax at 66% (2:1 ratio)
    growBy: new NumberRange(0, 2)
  }));
  
  // Add some series to make it clear which axis is which
  const xValues = Array.from(Array(100).keys());
  const yValues = xValues.map(x => Math.sin(x*0.3) * (150-x));
  
  // Blue series on primary X,Y axis
  sciChartSurface.renderableSeries.add(new FastLineRenderableSeries(wasmContext, {
    dataSeries: new XyDataSeries(wasmContext, { xValues, yValues }),
    stroke: "0066ff77", 
    strokeThickness: 5
  }));

  // Orange series on secondary, transposed, X,Y axis
  sciChartSurface.renderableSeries.add(new FastLineRenderableSeries(wasmContext, {
    yAxisId: "SecondaryYAxis",
    xAxisId: "SecondaryXAxis",
    dataSeries: new XyDataSeries(wasmContext, { xValues, yValues }),
    stroke: "ff660099", 
    strokeThickness: 5
  }));
  
  // Add panning, zooming interaction 
  sciChartSurface.chartModifiers.add(
    new ZoomPanModifier(),
    new MouseWheelZoomModifier(),
    new ZoomExtentsModifier()
  );
}

initSciChart("scichart-root");
```

This results in the following output:

{% codepen https://codepen.io/claireparker/pen/poGMpzb %}

# Building the Volume Profile Chart

Now that we’ve covered the basics of how to achieve the layout of a docked, rotated JavaScript chart inside the main chart, we can proceed to build a volume profile.

## Setting up a JavaScript Candlestick Chart with Market Data

With a chart setup as above with transposed X,Y axis, it's now possible to extend it as follows to draw real market data, volume bars, and a rotated volume profile chart.

To fetch the market data we're using a simple REST client for Binance which can be found on Github [here](https://github.com/ABTSoftware/SciChart.JS.Examples/blob/master/ExampleDependencies/src/ExampleData/binanceRestClient.ts). To fetch candle data, call this code. The return values are arrays of dates, open, high, low, close and volume values.

```javascript
// Data format is { dateValues[], openValues[], highValues[], lowValues[], closeValues[] }
const { dateValues, openValues, highValues, lowValues, closeValues, volumeValues }
     = await getCandles("BTCUSDT", "1h", 100);
```

Open, High, Low, Close values can be plotted on the chart as a candlestick series, while volume values can be plotted as a Column series. There are some extra tricks in SciChart.js to ensure the columns are docked bottom, and colored according to red/green bars in the main candle series. 

```javascript
// Plotting the market data as candles
// 

const candleDataSeries = new OhlcDataSeries(wasmContext, {
	xValues: dateValues,
	openValues,
	highValues,
	lowValues,
	closeValues,
});

// Create and add the Candlestick series
const candlestickSeries = new FastCandlestickRenderableSeries(wasmContext, {
	strokeThickness: 1,
	dataSeries: candleDataSeries,
	dataPointWidth: 0.7,
	brushUp: "#33ff3377",
	brushDown: "#ff333377",
	strokeUp: "#77ff77",
	strokeDown: "#ff7777",
});
sciChartSurface.renderableSeries.add(candlestickSeries);

// Plotting the volume data as columns
// 

// Helper class to colour column series according to price up or down
class VolumePaletteProvider extends DefaultPaletteProvider {    

    constructor(masterData, upColor, downColor) {
        super();
        this.upColorArgb = parseColorToUIntArgb(upColor);
        this.downColorArgb = parseColorToUIntArgb(downColor);
        this.ohlcDataSeries = masterData;
    }    

    // Return up or down color for the volume bars depending on Ohlc data
    overrideFillArgb(xValue, yValue, index, opacity, metadata) {
        const isUpCandle =
            this.ohlcDataSeries.getNativeOpenValues().get(index) >=
            this.ohlcDataSeries.getNativeCloseValues().get(index);
        return isUpCandle ? this.upColorArgb : this.downColorArgb;
    }
}

// Ensure the main chart has a hidden yAxis for volume bars 
const volumeYAxis = new NumericAxis(wasmContext, {
	id: "volumeAxisId",
	isVisible: false,
	growBy: new NumberRange(0, 4)    
});
sciChartSurface.yAxes.add(volumeYAxis);

// Add a volume series docked to bottom of the chart
const volumeSeries = new FastColumnRenderableSeries(wasmContext, {
	dataPointWidth: 0.7,
	strokeThickness: 0,
	dataSeries: new XyDataSeries(wasmContext, { xValues: dateValues, yValues: volumeValues }),
	yAxisId: "volumeAxisId",
	paletteProvider: new VolumePaletteProvider(
	  candleDataSeries,
	  "#33ff3377",
	  "#ff333377"
	),
});
sciChartSurface.renderableSeries.add(volumeSeries);
```

## Calculating the Volume Profile distribution

For this bit I used GPT-4 to save myself some time doing maths :D 

Here’s my prompt:

> **Prompt:** I’d like to calculate an approximation of a volume profile from candlestick data in JavaScript Given arrays of data in JavaScript containing open, high, low, close, volume data respectively.
> 
> `const dateValues = []; const openValues = []; const highValues = []; const lowValues = []; const closeValues = []; const volumeValues = [];`
> 
> Assume that the volume profile price is in bins. You can specify the bin size as a function parameter. The output should be two arrays: xValues[] should be prices and yValues[] should be computed volume profile values in bins.

The answer it provided was relatively decent, with a code sample on how to compute a volume distribution given arrays of Open, High, Low, Close and Volume values. Here’s the code to calculate the volume distribution based on all candle data. Adjusting this and recalculating for a specific range of candle data should be quite easy:

```javascript
const binSize = 25.0; // Define your bin size

// Function to calculate the bin for a given price
function getBin(price, binSize) {
	return Math.floor(price / binSize) * binSize;
}

// Initialize volume profile
const volumeProfile = {};

// Function to distribute volume across bins
function distributeVolume(high, low, volume, binSize) {
	const startBin = getBin(low, binSize);
	const endBin = getBin(high, binSize);

	let totalBins = (endBin - startBin) / binSize + 1;
	const volumePerBin = volume / totalBins;

	for (let bin = startBin; bin <= endBin; bin += binSize) {
		if (volumeProfile[bin]) {
			volumeProfile[bin] += volumePerBin;
		} else {
			volumeProfile[bin] = volumePerBin;
		}
	}
}

// Process each candlestick
for (let i = 0; i < highValues.length; i++) {
	distributeVolume(highValues[i], lowValues[i], volumeValues[i], binSize);
}

const xVolValues = [];
const yVolValues = [];

// Extract bins (prices) and corresponding volumes from volumeProfile
for (const [price, volume] of Object.entries(volumeProfile)) {
  xVolValues.push(parseFloat(price)); // Convert string key back to number
  yVolValues.push(volume);
}
```

The computed Volume Profile data can be plotted in SciChart on a transposed X,Y axis docked to the right of the chart as follows:

```javascript
// Create the transposed volume X-axis 
const volXAxis = new NumericAxis(wasmContext, {
	id: "VolX", 
	axisAlignment: EAxisAlignment.Right,
	flippedCoordinates: true,
	isVisible: false,
});
sciChartSurface.xAxes.add(volXAxis);

// Create the transposed volume Y-axis
sciChartSurface.yAxes.add(new NumericAxis(wasmContext, {
	id: "VolY",
	axisAlignment: EAxisAlignment.Bottom,
	isVisible: false,
	growBy: new NumberRange(0, 3)
}));

// When the main chart price yaxis changes, we want to update the range of the volume xAxis
priceYAxis.visibleRangeChanged.subscribe(args => {
	volXAxis.visibleRange = new NumberRange(args.visibleRange.min, args.visibleRange.max)
});

// ...

// Render the volume profile series on transposed Y, X axis. This could also be a 
// mountain series, stacked mountain series, and adding Point of Control (POC) is possible
// via Line Annotations
const volumeProfileSeries = new FastColumnRenderableSeries(wasmContext, {
  dataSeries: new XyDataSeries(wasmContext, { xValues: xVolValues, yValues: yVolValues }),
  dataPointWidth: 0.5,
  opacity: 0.33,
  fill: "White",
  strokeThickness: 0,
  xAxisId: "VolX",
  yAxisId: "VolY"
});
sciChartSurface.renderableSeries.add(volumeProfileSeries);
```

Here’s the final result, also with all the code present: the fetching of candlestick data from Binance US, plotting a JavaScript Candlestick chart, plotting coloured volume bars docked to the bottom of the chart, plotting a Volume Profile docked to the right of the chart, with computed volume distribution from live market data, and with zooming, panning and mousewheel zoom behaviour.

Enjoy!

{% codepen https://codepen.io/scichart/pen/YzJZgvd %}

#Future work & further enhancements

This sample could be expanded a lot further for use in real JS financial & trading applications.

- The Volume Profile could be rendered as a Stacked Mountain, Stacked Column or Mountain (Area) series
- The Bin size was hard coded to $25 in this example. In reality you'd probably want to vary this dynamically as the chart zooms.
- The volume profile distribution was calculated from all OHLC data, in practice you'd want to calculate this from visible data only, or a day-range
- You could add a Point of Control (POC) to the volume profile, using a HorizontalLineAnnotation at the price of maximum volume
- Using the SubCharts API in SciChart.js, you could place multiple charts inside the main chart to show the Volume Profile for specific ranges.
- Also using the SubCharts API, it could be placed on the axis rather than docked inside the main chart.