Close Editor Run Reset Auto Update CJS const lcjs = require('@lightningchart/lcjs')
const {
lightningChart,
AxisScrollStrategies,
AxisTickStrategies,
ColorRGBA,
PalettedFill,
LUT,
SolidFill,
SolidLine,
emptyLine,
regularColorSteps,
} = lcjs
const EXAMPLE_PATH = 'examples/assets/1718/'
const HISTORY_SECONDS = 15
const HISTORY_MS = HISTORY_SECONDS * 1000
const AREA_TARGET_POINTS = 96
const AREA_REFRESH_HZ = 24
const AREA_DECAY_STEPS = 12
const exampleContainer = document.getElementById('chart') || document.body
if (exampleContainer === document.body) {
exampleContainer.style.width = '100vw'
exampleContainer.style.height = '100vh'
exampleContainer.style.margin = '0px'
}
exampleContainer.style.display = 'flex'
exampleContainer.style.flexDirection = 'column'
const chartContainer = document.createElement('div')
exampleContainer.append(chartContainer)
chartContainer.style.width = '100%'
chartContainer.style.height = '100%'
const assetUrl = (fileName) => document.head.baseURI + EXAMPLE_PATH + fileName
const formatMHz = (hz) => `${(hz / 1_000_000).toFixed(3)}`
const loadArrayBuffer = async (url) => {
const response = await fetch(url)
if (!response.ok) throw new Error(`Failed to load ${url}: ${response.status}`)
return response.arrayBuffer()
}
const sliceRow = (array, rowIndex, binCount) => array.subarray(rowIndex * binCount, (rowIndex + 1) * binCount)
const makeAreaSpectrum = (row, minMHz, stepMHz, binAggregation, valueScale) => {
const pointCount = Math.ceil(row.length / binAggregation)
const xValues = new Float64Array(pointCount)
const yValues = new Float32Array(pointCount)
for (let i = 0; i < pointCount; i += 1) {
const iStart = i * binAggregation
const iEnd = Math.min(row.length, iStart + binAggregation)
let sum = 0
for (let j = iStart; j < iEnd; j += 1) {
sum += row[j] * valueScale
}
xValues[i] = minMHz + ((iStart + iEnd - 1) / 2) * stepMHz
yValues[i] = sum / (iEnd - iStart)
}
return { xValues, yValues }
}
const run = async () => {
const metadata = await fetch(assetUrl('metadata.json')).then((response) => response.json())
const spectrogramBuffer = await loadArrayBuffer(assetUrl(metadata.spectrogram.file))
const binCount = metadata.binCount
const rowCount = metadata.rowCount
const rowRate = metadata.rowRate
const rowStepMs = 1000 / rowRate
const frequencyMinMHz = metadata.frequencyAxis.minHz / 1_000_000
const frequencyMaxMHz = metadata.frequencyAxis.maxHz / 1_000_000
const frequencyStepMHz = (frequencyMaxMHz - frequencyMinMHz) / (binCount - 1)
const areaBinAggregation = Math.max(1, Math.round(binCount / AREA_TARGET_POINTS))
const isUint8Spectrogram = metadata.spectrogram.format === 'uint8'
const spectrogram = isUint8Spectrogram ? new Uint8Array(spectrogramBuffer) : new Float32Array(spectrogramBuffer)
const valueScale = metadata.spectrogram.normalizedScale ?? (isUint8Spectrogram ? 1 / 255 : 1)
const paletteMax = isUint8Spectrogram ? 255 : 1
const expectedValues = rowCount * binCount
if (spectrogram.length !== expectedValues) {
throw new Error(`RF asset shape mismatch. Expected ${expectedValues} values, got ${spectrogram.length}.`)
}
const lc = lightningChart()
const chart = lc
.ChartXY({
container: chartContainer,
defaultAxisX: { type: 'linear-highPrecision' },
defaultAxisY: { type: 'linear-highPrecision' },
legend: { visible: false },
})
.setTitle(`RF Monitoring | ${frequencyMinMHz.toFixed(3)}-${frequencyMaxMHz.toFixed(3)} MHz`)
.setCursorMode('show-nearest')
.setPadding({ left: 24 })
chart.yAxis.dispose()
const axisSpectrum = chart
.addAxisY({ iStack: 1 })
.setTitle('Normalized power')
.setInterval({ start: 0, end: 1 })
.setLength({ pixels: 180 })
.setMargins(10, 0)
.setUserInteractions({
zoom: false,
pan: false,
rectangleZoom: false,
restoreDefault: false,
chartInteractions: { zoom: false, pan: false, rectangleZoom: false, restoreDefault: false },
})
const axisSpectrogram = chart
.addAxisY({ iStack: 0, type: 'linear-highPrecision' })
.setScrollStrategy(AxisScrollStrategies.scrolling)
.setTickStrategy(AxisTickStrategies.DateTime, (s) =>
s.setCursorFormatter((value, range, locale) =>
new Date(value).toLocaleTimeString(locale, {
minute: '2-digit',
hour: '2-digit',
second: '2-digit',
hour12: false,
}),
),
)
.setDefaultInterval((state) => ({
end: state.dataMax ?? 0,
start: (state.dataMax ?? 0) - HISTORY_MS,
stopAxisAfter: false,
}))
chart.axisX
.setTitle('RF frequency')
.setUnits('MHz')
.setInterval({ start: frequencyMinMHz, end: frequencyMaxMHz })
.setUserInteractions({
zoom: {
mode: 'toward-pointer',
},
})
const areaSeriesList = new Array(AREA_DECAY_STEPS).fill(0).map((_, i) =>
chart
.addAreaSeries({ yAxis: axisSpectrum })
.setCurvePreprocessing({ type: 'spline' })
.setHighlightOnHover(false)
.setName(i === AREA_DECAY_STEPS - 1 ? 'Latest activity' : 'Activity history')
.setCursorEnabled(i === AREA_DECAY_STEPS - 1 ? true : false),
)
const defaultAreaColor = areaSeriesList[AREA_DECAY_STEPS - 1].getStrokeStyle().getFillStyle().getColor()
const areaStyles = new Array(AREA_DECAY_STEPS).fill(0).map((_, i) => {
const age = AREA_DECAY_STEPS - 1 - i
const fade = 1 - age / AREA_DECAY_STEPS
const strokeAlpha = Math.round(210 * fade)
const fillAlpha = Math.round(120 * fade)
const strokeColor = ColorRGBA(defaultAreaColor.getR(), defaultAreaColor.getG(), defaultAreaColor.getB(), strokeAlpha)
const fillColor = ColorRGBA(defaultAreaColor.getR(), defaultAreaColor.getG(), defaultAreaColor.getB(), fillAlpha)
return {
stroke: new SolidLine({ thickness: 1, fillStyle: new SolidFill({ color: strokeColor }) }),
fill: new SolidFill({ color: fillColor }),
}
})
areaSeriesList.forEach((series, i) => {
const style = areaStyles[i]
series
.setStrokeStyle(emptyLine)
// .setStrokeStyle(style.stroke)
.setAreaFillStyle(style.fill)
})
const theme = chart.getTheme()
const palette = new PalettedFill({
lookUpProperty: 'value',
lut: new LUT({
interpolate: true,
steps: regularColorSteps(0, paletteMax, theme.examples.spectrogramColorPalette),
units: 'power',
}),
})
const heatmapSeries = chart
.addHeatmapScrollingGridSeries({
scrollDimension: 'rows',
resolution: binCount,
yAxis: axisSpectrogram,
})
.setName('Power')
.setStep({ x: frequencyStepMHz, y: rowStepMs })
.setFillStyle(palette)
.setWireframeStyle(emptyLine)
.setIntensityInterpolation('disabled')
.setAggregation('max')
.setDataCleaning({ minDataPointCount: Math.ceil(HISTORY_SECONDS * rowRate * 1.5) })
let rowIndex = 0
let tPrevious = performance.now()
let rowCarry = 0
let valuesPushed = 0
let tStats = performance.now()
let tLastArea = 0
let tFirstRow
let nextHeatmapSampleIndex = 0
const areaHistory = []
const baseTitle = chart.getTitle()
const collectRows = (count) => {
const rows = []
for (let i = 0; i < count; i += 1) {
rows.push(sliceRow(spectrogram, (rowIndex + i) % rowCount, binCount))
}
return rows
}
const appendRows = (count) => {
if (count <= 0) return
if (!tFirstRow) {
tFirstRow = Date.now()
heatmapSeries.setStart({ x: frequencyMinMHz, y: tFirstRow })
}
const rows = collectRows(count)
heatmapSeries.invalidateIntensityValues({
iSample: nextHeatmapSampleIndex,
values: rows,
})
nextHeatmapSampleIndex += count
const latestSourceRow = (rowIndex + count - 1) % rowCount
if (performance.now() - tLastArea >= 1000 / AREA_REFRESH_HZ) {
const latestRow = sliceRow(spectrogram, latestSourceRow, binCount)
areaHistory.unshift(makeAreaSpectrum(latestRow, frequencyMinMHz, frequencyStepMHz, areaBinAggregation, valueScale))
areaHistory.length = Math.min(areaHistory.length, AREA_DECAY_STEPS)
areaSeriesList.forEach((series, i) => {
const historyIndex = AREA_DECAY_STEPS - 1 - i
const area = areaHistory[historyIndex]
series.clear()
if (area) series.appendSamples(area)
})
tLastArea = performance.now()
}
rowIndex += count
valuesPushed += count * binCount
}
const animate = () => {
const now = performance.now()
const elapsedSeconds = (now - tPrevious) / 1000
let rowsToAppend = elapsedSeconds * rowRate + rowCarry
rowCarry = rowsToAppend % 1
rowsToAppend = Math.floor(rowsToAppend)
tPrevious = now
appendRows(rowsToAppend)
if (now - tStats >= 1000) {
const pointsPerSecond = Math.round((valuesPushed * 1000) / (now - tStats))
chart.setTitle(`${baseTitle} | ${pointsPerSecond.toLocaleString()} values/s`)
valuesPushed = 0
tStats = now
}
requestAnimationFrame(animate)
}
const initialRows = Math.min(rowCount, Math.ceil(HISTORY_SECONDS * rowRate))
tFirstRow = Date.now() - (initialRows - 1) * rowStepMs
heatmapSeries.setStart({ x: frequencyMinMHz, y: tFirstRow })
appendRows(initialRows)
tPrevious = performance.now()
requestAnimationFrame(animate)
}
run().catch((error) => {
console.error(error)
const message = document.createElement('pre')
message.textContent = error && error.stack ? error.stack : String(error)
message.style.padding = '16px'
message.style.margin = '0px'
exampleContainer.append(message)
})
RF Monitoring and Spectrum Surveillance - Editor This example visualizes 2.4 GHz RF activity as a real-time spectrum surveillance view.
LightningChart JS renders a scrolling spectrogram for recent signal history and a fading area trace for the latest frequency-domain activity. The dataset contains BLE-like hopping bursts, Wi-Fi-like channels, LTE/5G-like allocations, persistent carriers, and unknown bursts.