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
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
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
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
Please, download the project template that is provided in this article.
You will see a file tree like this one:
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:
- Head
- Style
- 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>
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' } )
The 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.
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 )
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 )
}
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
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
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,
})
- 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 )
3D Ellipsoid
- importing the IIFE files:
<script src="lcjs.iife.js"></script>
- Extracting required parts from LightningCharts JS:
const {
lightningChart,
PointStyle3D,
ColorShadingStyles,
Themes
} = lcjs
- 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))
- Creating an instance for the scatter and ellipsoid:
const scatterSeries = chart3D.addPointSeries().setName('Scatter series')
const confidenceEllipsoidSeries = chart3D.addPointSeries().setName('Confidence ellipsoid')
- 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 }
})
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)
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 })
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>
- Extracting required parts from LightningCharts JS:
const {
lightningChart,
AxisTickStrategies,
Themes
} = lcjs
// Extract required parts from xyData.
const {
createProgressiveTraceGenerator
} = xydata
- 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('')
- 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
},
]
- 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)
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
}))
})
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)
})
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