DEV Community

Cover image for HTML Charts with JavaScript (Pt. 2)
Omar Urbano | LightningChart
Omar Urbano | LightningChart

Posted on

HTML Charts with JavaScript (Pt. 2)

Hi again :)

I recently brought to you the first part of this series of HTML Charts with JavaScript.

Today, I'll continue with three new charts you can create with basic HTML code.

This tutorial is not only perfect for new developers but also for those who are in data visualization and need to create simple yet stunning charting applications.

Let's see the charts:

- 3D Scatter Charts

3D-Scatter-Chart

In a 3D scatter chart, data points are represented by markers or symbols that are positioned in three-dimensional space based on the values of the three variables being plotted.

The X, Y, and Z-axis represent the three variables being plotted, and each data point has a corresponding X, Y, and Z value that determines its position in 3D space.

- 3D Box Series

3D-Box-Series

In a 3D JavaScript box series, each box represents a single data point and is positioned based on its x, y, and z coordinates.

The length, width, and height of the box can be used to represent additional variables, allowing for the visualization of complex relationships between multiple data dimensions.

The 3D nature of the box series allows for a more immersive and interactive data exploration experience, as users can rotate and zoom in on the visualization to gain different perspectives and insights.

- 3D Chunked Surface Grid Chart

3D-Chunked-Surface-Grid-Chart

The Chunked Surface Grid Chart breaks down a large set of data points into smaller chunks or tiles, which are then rendered in a three-dimensional grid.

Each tile represents a subset of the data, and its color or height represents a variable being plotted.

The 3D nature of the Chunked Surface Grid Chart allows for the visualization of complex relationships between multiple variables, such as time, location, and temperature.

But let's start getting our template ready...


Template Setup

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

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

3D-HTML-Charts-File-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.


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 Scatter Chart

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.

Extracting required parts from LightningChart JS:

const {
            lightningChart,
            SolidFill,
            ColorRGBA,
            PointStyle3D,
            Themes
        } = lcjs

const {
                createWaterDropDataGenerator
        } = xydata
Enter fullscreen mode Exit fullscreen mode

Let's start the chart object

const chart3D = lightningChart().Chart3D({
            disableAnimations: true,
            theme: Themes.darkGold,
        })
            .setTitle('3D Scatter Chart')
Enter fullscreen mode Exit fullscreen mode

Set titles to each axis

chart3D.getDefaultAxisX().setTitle('Axis X')
        chart3D.getDefaultAxisY().setTitle('Axis Y')
        chart3D.getDefaultAxisZ().setTitle('Axis Z')
Enter fullscreen mode Exit fullscreen mode

The getDefaultAxis, allow us to set UI properties to a specific axis.

Point Series for Y coordinates

const pointSeriesMaxCoords = chart3D.addPointSeries()
            .setPointStyle(new PointStyle3D.Triangulated({
                fillStyle: new SolidFill({ color: ColorRGBA(224, 152, 0) }),
                size: 10,
                shape: 'sphere'
            }))
            .setName('Max coords')
Enter fullscreen mode Exit fullscreen mode
  • [addPointSeries]: This is a method for adding a new PointSeries to the chart.

This series type visualizes a list of Points (pair of X and Y coordinates), with configurable markers over each coordinate.

PointSeries is optimized for massive amounts of data - here are some reference specs to give you an idea:

  1. A static data set in tens of millions range is rendered in a matter of seconds.

  2. With streaming data, even millions of data points can be streamed in every second, while retaining an interactive document. Read more about this in the pointSeries documentation.

  • [Triangulated]: A style class used to specify style of 3D points rendering as triangulated Cubes.

Heatmap data

Here we'll generate all the heatmap data to represent t he number of points scattered along the XZ plane.

createWaterDropDataGenerator()
            .setRows( rows )
            .setColumns( columns )
            .generate()
            .then( data => {
Enter fullscreen mode Exit fullscreen mode
  • Water drops data generator. It generates a data grid that contains "water droplets".

Water droplets are like dots of the most exposed area in the generated heatmap data. The generated data range depends on the WaterDropDataOptions.

More about WaterDropDataOptions in GitHub.

Data

Data refers to a number of the type Matrix number[][], that can be read as data[row][column].

It generates a value amount of points along this XZ coordinate, with the Y coordinate range based on the value.

.then( data => {

                for ( let row = 0; row < rows; row ++ ) {
                    for ( let column = 0; column < columns; column ++ ) {
                        const value = data[row][column]
                        // Generate 'value' amount of points along this XZ coordinate,
                        // with the Y coordinate range based on 'value'.
                        const pointsAmount = Math.ceil( value / 100 )
                        const yMin = 0
                        const yMax = value
                        for ( let iPoint = 0; iPoint < pointsAmount; iPoint ++ ) {
                            const y = yMin + Math.random() * (yMax - yMin)
                            pointSeriesOtherCoords.add({ x: row, z: column, y })
                            totalPointsAmount ++
                        }
                        pointSeriesMaxCoords.add({ x: row, z: column, y: yMax })
                        totalPointsAmount ++
                    }
                }
Enter fullscreen mode Exit fullscreen mode

3D Box Series

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.

Extracting required parts from LightningChart JS:

const {
            lightningChart,
            AxisScrollStrategies,
            PalettedFill,
            ColorRGBA,
            LUT,
            UILayoutBuilders,
            UIOrigins,
            UIElementBuilders,
            Themes
        } = lcjs

        const {
                createWaterDropDataGenerator
        } = xydata
Enter fullscreen mode Exit fullscreen mode

Let's start the chart object

const chart3D = lightningChart().Chart3D( {
            disableAnimations: true,
            theme: Themes.darkGold,
        } )
            .setTitle( 'BoxSeries3D with rounded edges enabled' )
Enter fullscreen mode Exit fullscreen mode

Set titles to each axis

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

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

        chart3D.getDefaultAxisZ()
            .setTitle( 'Z' )
Enter fullscreen mode Exit fullscreen mode

The getDefaultAxis, allow us to set UI properties to a specific axis.

ScrollStrategies can be used to establish the behavior of the axis scrolling. Read more about it in the axisScrollStrategies documentation.

PalletedFill

Now, let's set up the PalettedFill to dynamically colored boxes by an associated value property:

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 instances, 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.

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' } ) )
            .setRoundedEdges( 0.4 )
Enter fullscreen mode Exit fullscreen mode

Creating height map data

createWaterDropDataGenerator()
            .setRows( resolution )
            .setColumns( resolution )
            .generate()
            .then( waterdropData => {
                let t = 0
                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 )
                        }
                    }

                    boxSeries
                        .invalidateData( result )

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

The resolution variable is equal to 10. The loop will create 10 boxes (0 -9), assigning a height value depending of the number of the box. Each box is stored in the result array object

InvalidateData

Method for invalidating Box data. Accepts an Array of BoxDataCentered objects. Properties that must be defined for each NEW Box:

  • "xCenter", "yCenter", "zCenter" | coordinates of Box in Axis values.
  • "xSize", "ySize", "zSize" | size of Box in Axis values. ( if altering a previously created Box, these are not necessary )

Optional properties:

  • id: If supplied, the Box can be altered afterwards by supplying different data with the same "id".
  • color: If supplied, the Box will be coloured with that color, but only when the BoxSeries is styled as IndividualPointFill.
  • value: Look-up value to be used when the BoxSeries is styled as PalettedFill.

Learn more about it in the invalidateData documentation.


3D Chunked Surface Grid Chart

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.

Extracting required parts from LightningChart JS:

const {
            lightningChart,
            LUT,
            ColorRGBA,
            PalettedFill,
            emptyLine,
            LegendBoxBuilders,
            ColorShadingStyles,
            Themes
        } = lcjs

        const {
            createWaterDropDataGenerator
        } = xydata
Enter fullscreen mode Exit fullscreen mode

Starting the chart object

const chart = lightningChart().Chart3D({
            disableAnimations: true,
            theme: Themes.darkGold,
        })
Enter fullscreen mode Exit fullscreen mode

Adding the surface grid to the chart object:

const surfaceGrid = chart.addSurfaceGridSeries({
            columns: COLUMNS,
            rows: ROWS,
        })
Enter fullscreen mode Exit fullscreen mode

X & Z limits

const COLUMNS = 2000
        const ROWS = 2000
        const CHUNK_SIZE = 1000
Enter fullscreen mode Exit fullscreen mode

Color properties

.setColorShadingStyle(new ColorShadingStyles.Phong())
            .setFillStyle(
                new PalettedFill({
                    lookUpProperty: 'y',
                    lut: new LUT({
                        interpolate: false,
                        steps: [
                            { value: 0, label: '0', color: ColorRGBA(0, 0, 0) },
                            { value: 15, label: '15', color: ColorRGBA(0, 255, 0) },
                            { value: 30, label: '30', color: ColorRGBA(255, 0, 0) },
                            { value: 40, label: '40', color: ColorRGBA(0, 0, 255) },
                            { value: 50, label: '50', color: ColorRGBA(255, 255, 0) },
                            { value: 75, label: '75', color: ColorRGBA(0, 255, 255) },
                        ],
                    }),
                }),
            )
            .setWireframeStyle(emptyLine)
Enter fullscreen mode Exit fullscreen mode

LUT instances, 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.

  • setWireFrameStyle: Wireframe is a line grid that highlights the edges of each cell of the 3D surface. Read more about in the setWireFrameStyle documentation.

Adding horizontal legend boxes to the chart

const legend = chart.addLegendBox(LegendBoxBuilders.HorizontalLegendBox).add(chart)
            // Dispose example UI elements automatically if they take too much space. This is to avoid bad UI on mobile / etc. devices.
            .setAutoDispose({
                type: 'max-width',
                maxWidth: 0.80,
            })
Enter fullscreen mode Exit fullscreen mode

LegendBox

Load dataset in one "chunk" at a time.

Chunk refers to a smaller sub set of the entire data set. Loading large data sets in parts is extremely efficient in terms of memory usage and application usability.

;(async () => {
            const chunks = []
            for (let column = 0; column < COLUMNS; column += CHUNK_SIZE) {
                for (let row = 0; row < ROWS; row += CHUNK_SIZE) {
                    chunks.push({column, row})
                }
            }
Enter fullscreen mode Exit fullscreen mode

In this chart, we used the [createWaterDropDataGenerator] and [invalidateHeightmap] functions that were used in the previous chart.

const chunkData = await createWaterDropDataGenerator()
                    .setColumns(CHUNK_SIZE)
                    .setRows(CHUNK_SIZE)
                    .setWaterDrops(waterdropOptions)
                    .generate()

                surfaceGrid.invalidateHeightMap({
                    iColumn: chunk.column,
                    iRow: chunk.row,
                    values: chunkData,
                })
Enter fullscreen mode Exit fullscreen mode

We need the number of columns, rows and options. The main difference is that the values are created randomly:

const waterdropOptions = new Array(waterDropsCount).fill(0).map((_) => ({
                    rowNormalized: rand(0.0, 1.0),
                    columnNormalized: rand(0.0, 1.0),
                    amplitude: rand(5, 60),
                }))
Enter fullscreen mode Exit fullscreen mode

Conclusion

Now we've finished this series of HTML charts with JS. You can see the first part where we built an HTML template for 2D charts.

In this article, we learned about using 3D charts and how libraries (in this case LC JS) can help us fasten the development of charting applications.

As you saw, LightningChart JS is a fully customizable library that requries single lines of code to change properties that benefit end-users and their experience when using our(your) application.

If you're in Data Science or Data Visualization or are just a data viz enthusiast, developing high-performance plotting apps with familiar languages can help you a lot :).

Also, there's a bunch of charts available in the LC JS gallery. Check them out and let me know if there's any + "insert_here_technology" that you'd like me to write about!

See you in the next article!

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

Top comments (0)