HTML Charts with JavaScript (Pt. 1)

Omar Urbano | LightningChart - Mar 6 '23 - - Dev Community

Hi again!

Previously, I wrote an article about creating HTML charts and their easy implementation using JS charting frameworks but the article was limited to creating 2D charts in HTML with LightningChart JS.

Today, I bring you something more interesting: how to create 3D charts with basic HTML and the "LCJS" (LightningChart JS) library.

LightningCharts can be used as embed code, so it can be very useful if you don't have enough experience with advanced web development (MVC with C#, Java, Angular, etc.).

Today, we will create three charts:

- 3D Box Series Spectrogram

3D-Box-Series-Spectrogram
Simply put, an spectrogram is a visualization of sound and it plots frequency, time, and amplitude.

The frequencies are indicated by the color and height. That is very clear in the spectrogram above.

So, just think about it for a second, you can actually create this chart just using basic HTML and a JS framework.

This chart is typically used for acoustics, music, physics, and engineering fields.

- 3D Ellipsoid

3D-Ellipsoid

On the other hand, we're also creating a more scientific chart. The ellipsoid is frequently used in scientific fields as mathematics, physics, or engineering.

An ellipsoid is a sphere fed by several data points whose sum of distances is known as "forci".

In Astronomy, problems about celestial bodies are explained using ellipsoid charts. FYI: another type of chart used to describe celestial bodies' phenomena are the N-body simulations.

- 3D Line Series

3D-Line-Series

Finally, we're creating a more common visualization: a line series.

A line series is simply a visualization that connects data points with lines and in this case, in a three-dimension way. That is, for instance, plotting time, value, and an additional axis (depth).

Let's now jump to implementation.

Setting up our template

  1. Please, download the project template that is provided in this article.

  2. You will see a file tree like this one:

HTML-3D-Charts-filte-Tree

In this example we won’t need Node JS to compile our templates. We only need the HTML files and have a web browser.

Let’s code.

HTML Template

In the three HTML files, we will see the same structure:

  1. Head
  2. Style
  3. Body
  • In the Head section, we will specify the properties of the page.

  • In the Style section we will specify the style properties (CSS) for the template.

  • In the Body section, we will embed our JavaScript code.

For HTML, we'll use the <script> tag to work with client-side JavaScript. Also, notice that the HTML template is very simplified but you can edit it and make it more complex as you like.

3D Box Series Spectrogram

To import the IIFE files

<script src="xydata.iife.js"></script>
    <script src="lcjs.iife.js"></script>
Enter fullscreen mode Exit fullscreen mode

The IIFE file (Immediate Invoked Function Expression), contains all the Lightning Chart functions and properties that we need to create charts.

Setting PalettedFill to dynamically color boxes by an associated 'value' property:

const lut = new LUT( {
            steps: [
                { value: 15, 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 ) }
            ],
            units: 'dB',
            interpolate: true
        } )
        const paletteFill = new PalettedFill( { lut, lookUpProperty: 'y' } )
Enter fullscreen mode Exit fullscreen mode

The LUTinstances, like all LCJS-style classes, are immutable, which means that their setters do not modify the actual object, but instead return an entirely new modified object.
LUT Properties:

Steps: List of color steps (color + numeric value pair).
Dither: true enables automatic linear interpolation between color steps.

Creating the chart 3D object and configure the Axes:

const chart3D = lightningChart().Chart3D({
            disableAnimations: true,
            theme: Themes.darkGold,
        })
            .setTitle( '3D Box Series Spectrogram' )
            .setBoundingBox( { x: 1, y: 1, z: 2 } )
        chart3D.setCameraLocation( initialCameraLocation )
        chart3D.getDefaultAxisY()
            .setScrollStrategy( AxisScrollStrategies.expansion )
            .setInterval( 0, 100 )
            .setTitle( 'Power spectrum P(f)' )
        chart3D.getDefaultAxisX()
            .setTitle( 'Frequency (Hz)' )
        chart3D.getDefaultAxisZ()
            .setTitle( 'Time' )
            .setInterval( 0, -dataHistoryLength )
            .setScrollStrategy( AxisScrollStrategies.progressive )
Enter fullscreen mode Exit fullscreen mode

We can access to the Axes by using the [getDefaultAxisY - X] property.

  • ScrollStrategy: Specify ScrollStrategy of the Axis. This decides where the Axis scrolls based on current view and series boundaries.

  • Strategies: different strategies are available. Check them all in the Strategies documentation.

  • Themes: LightningChart JS has several themes available. For more details, check the Themes documentation.

DataGrid

Now we need to create a data grid of Boxes that we can mutate without creating new Boxes for a better performance.

const boxGrid = []
        for ( let sampleIndex = 0; sampleIndex < dataHistoryLength; sampleIndex++ ) {
            const sampleBoxIDs = []
            for ( let i = 0; i < dataSampleSize; i++ ) {
                const id = sampleIndex * dataSampleSize + i
                // Add empty Box to series.
                boxSeries.invalidateData( [{
                    id,
                    yMin: 0,
                    yMax: 0,
                    zMin: 0,
                    zMax: 0,
                    // Box X coordinates don't have to change afterwards.
                    xMin: i,
                    xMax: i + 1.0
                }] )
                sampleBoxIDs.push( id )
            }
            boxGrid.push( sampleBoxIDs )
        }
Enter fullscreen mode Exit fullscreen mode

Spectrum

createSpectrumDataGenerator()
            .setSampleSize( dataSampleSize )
            .setNumberOfSamples( dataHistoryLength )
            .setVariation( 5.0 )
            .generate()
            .setStreamRepeat( true )
            .setStreamInterval( 1000 / 60 )
            .setStreamBatchSize( 1 )
            .toStream()
            // Scale Y values from [0.0, 1.0] to [0.0, 80]
            .map( sample => sample.map( y => y * 80 ) )
            // Map Y values to a Row of Boxes.
            .forEach( sample => {
                const infiniteStreamingDataEnabled = toggleStreamingCheckBox.getOn()
                const addSample = infiniteStreamingDataEnabled || sampleIndex < dataHistoryLength
Enter fullscreen mode Exit fullscreen mode

The Spectrum data generator generates rows of random numbers that can be used to mimic spectrum data. Generated data is between 0 and 1.
To create a new instance of Spectrum data generator use createSpectrumDataGenerator() property.

UI controls

UI-control

In a charting application, UI controls refer to the graphical elements that allow users to interact with and customize charts.

These controls enable users to modify the appearance and behavior of charts to suit their needs and preferences.

Common UI controls include buttons, sliders, checkboxes, drop-down menus, and text boxes.

In this case, we'll use radio button selectors to control two main functions: Disable animations and infinite data stream option.

Is important to have UI controls in our application as they provide a user-friendly way of navigating and manipulating complex data sets.

This makes it easier for end-users to analyze and interpret the data while improving the user experience and overall effectiveness of the app.

  • UI control to stop the spectrum generator and leave the static chart.
const group = chart3D.addUIElement( UILayoutBuilders.Column
            .setBackground( UIBackgrounds.Rectangle )
        )
        group
            .setPosition( { x: 0, y: 100 } )
            .setOrigin( UIOrigins.LeftTop )
            .setMargin( 10 )
            .setPadding( 4 )
            // Dispose example UI elements automatically if they take too much space. This is to avoid bad UI on mobile / etc. devices.
            .setAutoDispose({
                type: 'max-height',
                maxHeight: 0.30,
            })
Enter fullscreen mode Exit fullscreen mode
  • UI control to toggle between infinite streaming data and static amount of data.
const handleStreamingToggled = ( state ) => {
            toggleStreamingCheckBox.setText( state ? 'Disable infinite streaming data' : 'Enable infinite streaming data' )
            if ( toggleStreamingCheckBox.getOn() !== state ) {
                toggleStreamingCheckBox.setOn( state )
            }
        }
        const toggleStreamingCheckBox = group.addElement( UIElementBuilders.CheckBox )
        toggleStreamingCheckBox.onSwitch(( _, state ) => handleStreamingToggled( state ) )
        handleStreamingToggled( true )
Enter fullscreen mode Exit fullscreen mode

3D Ellipsoid

  • importing the IIFE files:
<script src="lcjs.iife.js"></script>
Enter fullscreen mode Exit fullscreen mode
  • Extracting required parts from LightningCharts JS:
const {
            lightningChart,
            PointStyle3D,
            ColorShadingStyles,
            Themes
        } = lcjs
Enter fullscreen mode Exit fullscreen mode
  • Creating the chart 3D object and configuring the Axes:
const chart3D = lightningChart().Chart3D({
            theme: Themes.darkGold,
        }).setTitle('3D scatter data set and confidence ellipsoid')
        chart3D.forEachAxis((axis) => axis.setInterval(-1.8, 1.8, false, true))
Enter fullscreen mode Exit fullscreen mode
  • Creating an instance for the scatter and ellipsoid:
const scatterSeries = chart3D.addPointSeries().setName('Scatter series')
        const confidenceEllipsoidSeries = chart3D.addPointSeries().setName('Confidence ellipsoid')
Enter fullscreen mode Exit fullscreen mode
  • Creating random data for the scatter series:
const xSize = 1.2
        const ySize = 1.2
        const zSize = 1.5
        const pi2 = Math.PI * 2
        const data = new Array(1000).fill(0).map((_) => {
            const u = Math.random()
            const v = Math.random()
            const theta = u * pi2
            const phi = Math.acos(2.0 * v - 1.0)
            const r = Math.cbrt(Math.random())
            const sinTheta = Math.sin(theta)
            const cosTheta = Math.cos(theta)
            const sinPhi = Math.sin(phi)
            const cosPhi = Math.cos(phi)
            const x = xSize * r * sinPhi * cosTheta
            const y = ySize * r * sinPhi * sinTheta
            const z = zSize * r * cosPhi
            return { x, y, z }
        })
Enter fullscreen mode Exit fullscreen mode

Note: the mathematics here may be challenging. For additional help on those, read more about the SpherePointPicking and more about how to generate those random points.

Applying the examples to create coordinate points in an sphere, the process will create an array with 1000 points.

scatterSeries.add(data)
Enter fullscreen mode Exit fullscreen mode

Once we have the array data object, we can add it to the scatterSeries instance.

  • Creating the Ellipsoid
confidenceEllipsoidSeries
            .setDepthTestEnabled(false)
            .setColorShadingStyle(new ColorShadingStyles.Simple())
            .setPointStyle(
                new PointStyle3D.Triangulated({
                    // Ellipsoid is rendered as sphere with individual sizes along X, Y and Z axes.
                    shape: 'sphere',
                    size: { x: xSize * 2, y: ySize * 2, z: zSize * 2 },
                    fillStyle: scatterSeries.getPointStyle().getFillStyle().setA(50),
                }),
            )
            .add({ x: 0, y: 0, z: 0 })
Enter fullscreen mode Exit fullscreen mode

The Ellipsoid uses the limits specified in the xSize, ySize, zSize constants.

The color of the ellipsoid will be the same obtained in the Scatter series, adding the alpha property to 50%.

3D Line Series

  • Importing the iife files.
<script src="lcjs.iife.js"></script>
    <script src="xydata.iife.js"></script>
Enter fullscreen mode Exit fullscreen mode
  • Extracting required parts from LightningCharts JS:
const {
            lightningChart,
            AxisTickStrategies,
            Themes
        } = lcjs

        // Extract required parts from xyData.
        const {
            createProgressiveTraceGenerator
        } = xydata
Enter fullscreen mode Exit fullscreen mode
  • Creating the chart object and adding properties to the axes:
const chart3D = lightningChart().Chart3D({
            theme: Themes.darkGold,
        })
            // Set 3D bounding box dimensions to highlight X Axis. 
            .setBoundingBox({ x: 1.0, y: 0.5, z: 0.4 })

        // Set Axis titles
        chart3D.getDefaultAxisX().setTitle('Axis X')
        chart3D.getDefaultAxisY().setTitle('Axis Y')
        chart3D.getDefaultAxisZ().setTitle('')
Enter fullscreen mode Exit fullscreen mode
  • Creating the Series that will be displayed and the amount of data per series:
const seriesConf = [
            {
                name: 'Series A',
                dataAmount: 50,
            },
            {
                name: 'Series B',
                dataAmount: 50
            },
            {
                name: 'Series C',
                dataAmount: 50
            },
        ]
Enter fullscreen mode Exit fullscreen mode
  • Creating the series and generating the data:
seriesConf.forEach((conf, iSeries) => {
            const seriesName = conf.name || ''
            const seriesDataAmount = conf.dataAmount || 100
            const seriesZ = iSeries

            const series = chart3D.addPointLineSeries()
                .setName(seriesName)
Enter fullscreen mode Exit fullscreen mode

For each series in the SeriesConf array, the name and data amount will be taken from same array.

The series Z will be the number of the current series in the loop.

  • createProgressiveTraceGenerator:

A progressive trace data generator.

  • Generates point data that has a progressive X axis.
  • The data is always derived from the previous point.
  • The number of points will be specified in the seriesConf array.
const series = chart3D.addPointLineSeries()
                .setName(seriesName)

            createProgressiveTraceGenerator()
                .setNumberOfPoints(seriesDataAmount)
                .generate()
                .toPromise()
                .then((data) => {
                    // Map XY data to XYZ data.
                    return data.map((xy) => ({
                        x: xy.x,
                        y: xy.y,
                        z: seriesZ
                    }))
                })
Enter fullscreen mode Exit fullscreen mode

The data will be splitted into 3 members, one for each series.
Each class in the batch will contain the x, y and z values.

.then((data) => {
                    // Stream data into series very quickly.

                    setInterval(() => {
                        const batch = data.splice(0, 3)

                        if (batch.length > 0) {
                            console.log(batch)
                            series.add(batch)
                            totalDataAmount += batch.length
                            chart3D.setTitle(`3D Line Series (${totalDataAmount} data points)`)
                        }
                    }, 30)
                })
Enter fullscreen mode Exit fullscreen mode

X-Y-Z-VALUES

Conclusion

As we have seen, generating 3D graphics is possible embedded within a simple HTML template.

The look and feel properties are mostly provided by Lightning Charts, which reduces the time spent developing a style sheet.

In this exercise, we didn't use Node JS or some other framework that uses type script.

So if you need to work with advanced charts, but only have a traditional website, you shouldn't have a problem making use of the LightningChart JS library.

This is a first part of the 3D charts articles series, check out the second part here.

See you in the next article!

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

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