<div id="scichart-root" ></div>
  
body { margin: 0; }
#scichart-root { width: 100%; height: 100vh; }
  
  const {
    SciChartDefaults,
    SciChartSurface,
    CategoryAxis,
    NumericAxis,
    FastCandlestickRenderableSeries,
    OhlcDataSeries,
    SciChartJsNavyTheme,
    makeIncArray,
    ECoordinateMode,
    EAxisAlignment,
    FastColumnRenderableSeries,
    XyDataSeries,
    ENumericFormat,
    NumberRange,
    MouseWheelZoomModifier, 
    ZoomPanModifier, 
    ZoomExtentsModifier,
    IFillPaletteProvider,
    DefaultPaletteProvider,
    parseColorToUIntArgb,
    EAutoRange,
    EXyDirection
  } = SciChart;

// 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;
    }
  
    overrideStrokeArgb(xValue, yValue, index, opacity, metadata) { 
        return this.overrideFillArgb(xValue, yValue, index, opacity, metadata);
    }
}

// Helper function to fetch candlestick data from Binance via Rest API
const getCandles = async (
    symbol,
    interval,
    limit = 300
) => {
  let url = `https://api.binance.us/api/v3/klines?symbol=${symbol}&interval=${interval}`;
  if (limit) {
    url += `&limit=${limit}`;
  }
  try {
    console.log(`SimpleBinanceClient: Fetching ${limit} candles of ${symbol} ${interval}`);
    const response = await fetch(url);
    // Returned data format is [ { date, open, high, low, close, volume }, ... ]
    const data = await response.json();
    // Map to { dateValues[], openValues[], highValues[], lowValues[], closeValues[] } expected by scichart.js
    const dateValues = [];
    const openValues = [];
    const highValues = [];
    const lowValues = [];
    const closeValues = [];
    const volumeValues = [];
    data.forEach(candle => {
      const [timestamp, open, high, low, close, volume] = candle;
      dateValues.push(timestamp / 1000); // SciChart expects Unix Timestamp / 1000
      openValues.push(parseFloat(open));
      highValues.push(parseFloat(high));
      lowValues.push(parseFloat(low));
      closeValues.push(parseFloat(close));
      volumeValues.push(parseFloat(volume));
    });
    return { dateValues, openValues, highValues, lowValues, closeValues, volumeValues };
  } catch (err) {
    console.error(err);
    return [];
  }
};

// Initializes a volume profile chart with SciChart.js
async function volumeProfile(divElementId) {  
  SciChartDefaults.performanceWarnings = false;
  const { wasmContext, sciChartSurface } = await SciChartSurface.create(divElementId, {
    theme: new SciChartJsNavyTheme()
  });
  sciChartSurface.xAxes.add(new NumericAxis(wasmContext, { 
    labelFormat: ENumericFormat.Date_HHMMSS 
  }));  
  const priceYAxis = new NumericAxis(wasmContext, { 
    labelPrefix: "$", 
    labelPrecision: 2
  });
  sciChartSurface.yAxes.add(priceYAxis);
  const volumeYAxis = new NumericAxis(wasmContext, {
    id: "volumeAxisId",
    isVisible: false,
    growBy: new NumberRange(0, 4)    
  });
  sciChartSurface.yAxes.add(volumeYAxis);

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

  // Create a OhlcDataSeries with open, high, low, close values
  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);
 
  // 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);
  
  // 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 => {
    console.log(`${args.visibleRange.min}, ${args.visibleRange.max}`);
    volXAxis.visibleRange = new NumberRange(args.visibleRange.min, args.visibleRange.max)
  });

  sciChartSurface.zoomExtents();

  // This code provides faked normally distributed data for a volume profile
  // const volWidth = 10;
  // const numbars = Math.round(priceYAxis.visibleRange.diff / volWidth);
  // const xVolValues = makeIncArray(numbars, volWidth, n => n + priceYAxis.visibleRange.min);
  // const yVolValues = makeIncArray(
  //     numbars,
  //     1,
  //     v => Math.exp(-Math.pow(v - numbars / 2, 2) / 20) + Math.random() * 0.5
  // );
  
  // Whereas this code actually calculates the volume profile
  // 
  
  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);
  }
  
  // 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);
  
  // add interactivity for the example  
  // Keep the volume profile pinned to the right, by not modifying the volume y axis
  sciChartSurface.chartModifiers.add(new MouseWheelZoomModifier({ excludedYAxisIds: ["volumeAxisId", "VolY"], xyDirection: EXyDirection.XDirection }));
  sciChartSurface.chartModifiers.add(new ZoomPanModifier({ excludedYAxisIds: ["volumeAxisId", "VolY"]}));
  sciChartSurface.chartModifiers.add(new ZoomExtentsModifier());
};

volumeProfile("scichart-root");

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdn.jsdelivr.net/npm/scichart/index.min.js