Close Editor Run Reset Auto Update CJS /*
* LightningChartJS example for multi-channel real-time monitoring Line Chart.
*/
// Import LightningChartJS
const lcjs = require('@lightningchart/lcjs')
// Import xydata
const xydata = require('@lightningchart/xydata')
// Import data Generator
const { createProgressiveFunctionGenerator } = xydata
// Extract required parts from LightningChartJS.
const { lightningChart, SolidFill, SolidLine, AxisScrollStrategies, AxisTickStrategies, ColorRGBA, emptyFill, DataSetXY, Themes } = lcjs
const DATA_FREQUENCY_HZ = 1000
const CHANNELS_AMOUNT = 10
const xIntervalMax = 30 * DATA_FREQUENCY_HZ
const channelIntervalY = 2 // [-1, 1]
const chart = lightningChart()
.ChartXY({
// theme: Themes.darkGold
})
.setTitle(`Multi-channel real-time monitoring (${CHANNELS_AMOUNT} chs, ${DATA_FREQUENCY_HZ} Hz)`)
const axisX = chart
.getDefaultAxisX()
.setScrollStrategy(AxisScrollStrategies.scrolling)
.setDefaultInterval((state) => ({ end: state.dataMax, start: (state.dataMax ?? 0) - xIntervalMax, stopAxisAfter: false }))
.setTitle('Data points per channel')
const axisY = chart
.getDefaultAxisY()
.setTickStrategy(AxisTickStrategies.Empty)
.setTitle('< Channels >')
.setScrollStrategy(AxisScrollStrategies.expansion)
.setInterval({ start: -channelIntervalY / 2, end: CHANNELS_AMOUNT * channelIntervalY, stopAxisAfter: false })
const dataSet = new DataSetXY({
schema: {
x: { pattern: 'progressive' },
...Object.fromEntries(Array.from({ length: CHANNELS_AMOUNT }, (_, i) => [`y${i}`, { pattern: null }])),
},
}).setMaxSampleCount(xIntervalMax)
const series = new Array(CHANNELS_AMOUNT).fill(0).map((_, iChannel) => {
// Create line series optimized for regular progressive X data.
const nSeries = chart
.addLineSeries()
.setName(`Channel ${iChannel + 1}`)
// Use -1 thickness for best performance, especially on low end devices like mobile / laptops.
.setStrokeStyle((style) => style.setThickness(-1))
.setDataSet(dataSet, { x: 'x', y: `y${iChannel}` })
// Add custom tick for each channel.
chart
.getDefaultAxisY()
.addCustomTick()
.setValue((CHANNELS_AMOUNT - (1 + iChannel)) * channelIntervalY)
.setTextFormatter(() => `Channel ${iChannel + 1}`)
.setGridStrokeStyle(
new SolidLine({
thickness: 1,
fillStyle: new SolidFill({ color: ColorRGBA(255, 255, 255, 60) }),
}),
)
return nSeries
})
// Define unique signals that will be used for channels.
const signals = [
{ length: 100 * 2 * Math.PI, func: (x) => Math.sin(x / 100) },
{ length: 100 * 2 * Math.PI, func: (x) => Math.cos(x / 100) },
{
length: 200 * 2 * Math.PI,
func: (x) => Math.cos(x / 200) + Math.sin(x / 100),
},
{
length: 200 * 2 * Math.PI,
func: (x) => Math.sin(x / 50) + Math.cos(x / 200),
},
{
length: 200 * 2 * Math.PI,
func: (x) => Math.sin(x / 100) * Math.cos(x / 200),
},
{ length: 450 * 2 * Math.PI, func: (x) => Math.cos(x / 450) },
{ length: 800 * 2 * Math.PI, func: (x) => Math.sin(x / 800) },
{
length: 650 * 2 * Math.PI,
func: (x) => Math.sin(x / 200) * Math.cos(x / 650),
},
]
// Generate data sets for each signal.
Promise.all(
signals.map((signal) =>
createProgressiveFunctionGenerator()
.setStart(0)
.setEnd(signal.length)
.setStep(1)
.setSamplingFunction(signal.func)
.generate()
.toPromise()
.then((data) => data.map((xy) => xy.y)),
),
).then((dataSets) => {
// Stream data into series.
let tStart = window.performance.now()
let pushedDataCount = 0
const xStep = 1000 / DATA_FREQUENCY_HZ
const streamData = () => {
const tNow = window.performance.now()
// NOTE: This code is for example purposes (streaming stable data rate without destroying browser when switching tabs etc.)
// In real use cases, data should be pushed in when it comes.
const shouldBeDataPointsCount = Math.floor((DATA_FREQUENCY_HZ * (tNow - tStart)) / 1000)
const newDataPointsCount = Math.min(shouldBeDataPointsCount - pushedDataCount, 1000) // Add max 1000 data points per frame into a series. This prevents massive performance spikes when switching tabs for long times
const newData = {
x: [],
...Object.fromEntries(Array.from({ length: CHANNELS_AMOUNT }, (_, i) => [`y${i}`, []])),
}
for (let iDp = 0; iDp < newDataPointsCount; iDp++) {
const x = (pushedDataCount + iDp) * xStep
newData.x.push(x)
for (let iChannel = 0; iChannel < series.length; iChannel++) {
const dataSet = dataSets[iChannel % dataSets.length]
const iData = (pushedDataCount + iDp) % dataSet.length
const ySignal = dataSet[iData]
const y = (CHANNELS_AMOUNT - 1 - iChannel) * channelIntervalY + ySignal
newData[`y${iChannel}`].push(y)
}
}
dataSet.appendSamples(newData)
pushedDataCount += newDataPointsCount
requestAnimationFrame(streamData)
}
streamData()
})
// Measure FPS.
let tStart = window.performance.now()
let frames = 0
let fps = 0
const title = chart.getTitle()
const recordFrame = () => {
frames++
const tNow = window.performance.now()
fps = 1000 / ((tNow - tStart) / frames)
sub_recordFrame = requestAnimationFrame(recordFrame)
chart.setTitle(`${title} (FPS: ${fps.toFixed(1)})`)
}
let sub_recordFrame = requestAnimationFrame(recordFrame)
setInterval(() => {
tStart = window.performance.now()
frames = 0
}, 5000)
Real-time data monitoring JavaScript Chart - Editor Multi-channel real-time data monitoring Line Chart
Lightning-fast Line Chart visualization over multiple channels that progress on the same X Axis
Widely used in all kinds of fields for monitoring live data from many (hundreds or even thousands) of data sources at the same time.
Frames rendered per second (FPS ) is recorded live, and displayed on the Chart title. FPS of 40-60 indicates a smooth running performance.
Automatic data cleaning In infinitely scrolling applications, cleaning old out of view data is extremely crucial; in this example LineSeries.setMaxPointsCount method is used to enable automatic data cleaning. For reference, see also LineSeries.setDataCleaningThreshold.
The setMaxPointsCount method sets the amount of data points , that will always be retained in the series head (latest data). The full conditions for valid data cleaning are:
There is a considerably large chunk of data out of view (visible data is not cleaned!). If the chunk is removed, the amount of data that remains is still more than max points count . In practice, this results in an application where you can even scroll back for some distance and see older data, but if you scroll far enough, you will find that the old data has been cleaned . This allows the application to run forever !