Create a JavaScript 3D Box Chart with Rounded Edges

Omar Urbano | LightningChart - Jul 28 '22 - - Dev Community

Hello!

So this is my first article here and wanted to show you how I created a simple but amazing animated 3D Bar Chart with dynamically colored bars based on their height.

3D Box Chart

The chart is rendered using NodeJS, Visual Studio Code, and LightningChart JS' BoxSeries3D, a series type capable of rendering any number of Boxes as well as allowing single full-level modifications (location, size, color) at any point during the runtime.

The example also shows how to use the rounded edges feature. This is a characteristic of this chart that is active by default but can also be disabled (if you use BoxSeries3D.setRoundedEdges).

Ideally, you would download the project to follow this article. The file is a RAR and you can get it from here:
3D Box Series Project (RAR)

lcjs library

For those who aren't yet familiar with LightningChart JS, this is a high-performance library (also available for .NET) that features some 100+ chart types including XY charts, line series, surface charts, heatmaps, mapcharts, and other types as well.

A great benefit of the "lcjs" library is its high-performance capability to render million and billion of data points in real-time charting applications at high FPS and using minimal resources.

The lcjs library is also hardware accelerated and charts are WebGL rendered.

With that being said, let's set up our project template.

Setting up the template

  1. Please, download the template that is provided in this article.
  2. You will see a file tree like this one:

Project file tree

  1. Now, open a new terminal in Visual Studio Code
  2. As usual in a Node JS project, we will have to run our NPM Install command to begin with.

This would be everything for our initial setup.

And now let's code :)

CHART.ts

Inside this file, you'll see all the logic needed to create our chart, configure animations, and format the data.

  1. Declare the constant lcjs that will refer to our @arction/lcjs library.
  2. Extract required classes from lcjs.
const lcjs = require('@arction/lcjs')

// Extract required parts from LightningChartJS.
const {
    lightningChart,
    AxisScrollStrategies,
    PalettedFill,
    ColorRGBA,
    LUT,
    UILayoutBuilders,
    UIOrigins,
    UIElementBuilders,
    Themes
} = lcjs
Enter fullscreen mode Exit fullscreen mode
  1. Creating the chart object:
const chart3D = lightningChart().Chart3D( {
    disableAnimations: true,
    theme: Themes.lightNew,
} )
    .setTitle( 'BoxSeries3D with rounded edges enabled' )
Enter fullscreen mode Exit fullscreen mode
  • disableAnimations:
    Disable all animations for the chart.
    After calling this function, animations (Zooming, scaling) for all Axes will be disabled. Animations must be recreated manually afterward.

  • Theme:
    The LightningChart library offers a collection of default implementations that can be accessed by Themes.

The Color theme of the components must be specified when they are created and can't be changed afterwards (without destroying and recreating the component). More info about this here.

chart3D.getDefaultAxisY()
    .setScrollStrategy( AxisScrollStrategies.expansion )
    .setTitle( 'Height' )

chart3D.getDefaultAxisX()
    .setTitle( 'X' )

chart3D.getDefaultAxisZ()
    .setTitle( 'Z' )
Enter fullscreen mode Exit fullscreen mode
  • getDefaultAxisY: Gets the Y axis.
    setScrollStrategy: Specify ScrollStrategy of the Axis. This decides where the Axis scrolls based on current view and series boundaries.

  • getDefaultAxisX: Gets the X axis

  • getDefaultAxisz: Gets the z axis

const boxSeries = chart3D.addBoxSeries()
const resolution = 10
Enter fullscreen mode Exit fullscreen mode
  • const boxSeries: .addBoxSeries = Creates Series for visualization of large sets of individually configurable 3D Boxes.

  • const resolution: Constant that will affect the number of columns displayed in the 3D chart.
    Resolution = 50

3D Box Series with a resolution of 50

const lut = new LUT( {
    steps: [
        { value: 0, color: ColorRGBA( 0, 0, 0 ) },
        { value: 30, color: ColorRGBA( 255, 255, 0 ) },
        { value: 45, color: ColorRGBA( 255, 204, 0 ) },
        { value: 60, color: ColorRGBA( 255, 128, 0 ) },
        { value: 100, color: ColorRGBA( 255, 0, 0 ) }
    ],
    interpolate: true
} )
Enter fullscreen mode Exit fullscreen mode
  • LUT: (Look Up Table) Style class for describing a table of colors with associated lookup values (numbers).

Instances of LUT, like all LCJS style classes, are immutable, meaning that its setters do not modify the actual object, but instead return a completely new modified object.

Properties of LUT:

steps: List of color steps (color + number value pair).
interpolate: true enables automatic linear interpolation between color steps.

You can learn more about the classes used in lcjs here

  • boxSeries: Specify edge roundness. For applications with massive amounts of small Boxes, it is wise to disable for performance benefits.
boxSeries
    .setFillStyle( new PalettedFill( { lut, lookUpProperty: 'y' } ) )
    // Specify edge roundness.
    // For applications with massive amounts of small Boxes, it is wise to disable for performance benefits.
    .setRoundedEdges( 0.4 )
Enter fullscreen mode Exit fullscreen mode
  • createWaterDropDataGenerator: Configures the custom properties to the 3D chart. Some constants that we created before will be used in this method.
createWaterDropDataGenerator()
    .setRows( resolution )
    .setColumns( resolution )
    .generate()
Enter fullscreen mode Exit fullscreen mode

setRows: Creates and add the number of rows specified in the constant [resolution].

setColumns: Creates and add the number of columns specified in the constant [resolution].

const step = () => {
            const result = []
            for ( let x = 0; x < resolution; x++ ) {
                for ( let y = 0; y < resolution; y++ ) {
                    const s = 1
                    const height = Math.max(
                        waterdropData[y][x] +
                        50 * Math.sin( ( t + x * .50 ) * Math.PI / resolution ) +
                        20 * Math.sin( ( t + y * 1.0 ) * Math.PI / resolution ), 0 )
                    const box = {
                        xCenter: x,
                        yCenter: height / 2,
                        zCenter: y,
                        xSize: s,
                        ySize: height,
                        zSize: s,
                        // Specify an ID for each Box in order to modify it during later frames, instead of making new Boxes.
                        id: String( result.length ),
                    }
                    result.push( box )
                }
            }
Enter fullscreen mode Exit fullscreen mode
  • const step: This constant will create “water drops” equal to the number of resolution specified before.

waterdropData: Generates a grid of data containing "water drops", which are like spots of a more exposed area in the generated heatmap data. The generated data range depends on the WaterDropDataOptions.

To create a new instance of Water drop data generator use createWaterDropDataGenerator factory.

Each object created, will be added to the result array object.

boxSeries
    .invalidateData( result )

t += 0.1
requestAnimationFrame( step )
Enter fullscreen mode Exit fullscreen mode

The result array object with the entire collection will be added to the boxSeries object.

  • requestAnimationframe: Indicates to the browser thats a new animation needs to be created before the next repaint.

npm start:

Finally, open a new terminal and run the npm start command in the terminal to visualize the chart in a local server:

Image description

Image description

Final thoughts

Here's the final result

Image description

This was an easy chart and part of a new JS charts series that I'll be writing about.

The library used here is LightningChart JS that features from basic to highly specialized charts including XY, line series, heatmaps, surface charts, and this one, the 3D Box Series.


Written by:
Omar Urbano | Software Engineer
Find me on LinkedIn

. . . . . . . . . . . . . . . .