Close Editor Run Reset Auto Update CJS const lcjs = require('@lightningchart/lcjs')
const { createProgressiveTraceGenerator } = require('@lightningchart/xydata')
const { lightningChart, AxisPosition, Themes, AxisTickStrategies, synchronizeAxisIntervals, emptyLine, SolidFill } = lcjs
let channels = [{ name: 'Channel 1' }, { name: 'Channel 2' }, { name: 'Channel 3' }]
const exampleContainer = document.getElementById('chart') || document.body
exampleContainer.style.display = 'grid'
const scrollContainer = document.createElement('div')
scrollContainer.style.cssText = `
grid-area: 1 / 1;
overflow-y: auto;
`
exampleContainer.append(scrollContainer)
const container = document.createElement('div')
scrollContainer.append(container)
container.style.height = '200vh'
container.style.width = '100%'
const stickyXContainer = document.createElement('div')
stickyXContainer.style.cssText = `
grid-area: 1 / 1;
align-self: end;
overflow: hidden;
`
exampleContainer.append(stickyXContainer)
const lc = lightningChart()
const chart = lc
.ChartXY({
container,
legend: { visible: false },
// theme: Themes.darkGold
})
.setTitle('')
.setPadding({ top: 0 })
// Improves cursor visibility in use cases with HTML overlays between stacked axes
.setCursor((cursor) => cursor.setKeepWithinAxisBoundaries(true))
const isDarkTheme = chart.getTheme().isDark
const themeBackgroundFill = chart.getTheme().chartXYSeriesBackgroundFillStyle
const themeBackgroundColor =
typeof themeBackgroundFill.getColor === 'function'
? themeBackgroundFill.getColor()
: themeBackgroundFill.getColorStops()[themeBackgroundFill.getColorStops().length - 1].color
const backgroundFill = new SolidFill({ color: themeBackgroundColor.setA(255) })
chart.setBackgroundFillStyle(backgroundFill).setSeriesBackgroundFillStyle(backgroundFill)
chart.axisY.dispose()
chart.axisX.setTickStrategy(AxisTickStrategies.DateTime)
const createChannel = (info) => {
const axisY = chart.addAxisY({
iStack: -chart.getAxes(AxisPosition.Left).length,
})
const series = chart.addPointLineAreaSeries({ axisY }).setCurvePreprocessing({ type: 'spline' }).setName(info.name)
createProgressiveTraceGenerator()
.setNumberOfPoints(1000)
.generate()
.toPromise()
.then((data) => {
series.appendJSON(data, {
start: performance.timeOrigin,
step: 60 * 1000,
})
})
// HTML header UI that displays channel name and allows modifying the channel.
const headerDiv = document.createElement('div')
chart.engine.container.append(headerDiv)
headerDiv.style.display = 'flex'
headerDiv.style.alignItems = 'center'
headerDiv.style.justifyContent = 'space-between'
headerDiv.style.padding = '.2rem'
headerDiv.style.border = `1px solid ${isDarkTheme ? 'white' : 'black'}`
headerDiv.style.position = 'absolute'
headerDiv.style.backgroundColor = isDarkTheme ? 'black' : 'white'
headerDiv.style.fontFamily = 'Segoe UI'
headerDiv.style.width = '100%'
headerDiv.style.boxSizing = 'border-box'
const channelText = document.createElement('span')
headerDiv.appendChild(channelText)
channelText.textContent = info.name
channelText.style.flexGrow = '1'
const moveUpButton = document.createElement('button')
headerDiv.appendChild(moveUpButton)
moveUpButton.textContent = 'Up'
moveUpButton.style.background = 'transparent'
moveUpButton.style.border = 'none'
moveUpButton.style.color = 'inherit'
moveUpButton.onclick = () => {
const axes = chart.getAxes(AxisPosition.Left)
const i = axes.indexOf(axisY)
const axisAbove = axes[i + 1]
if (!axisAbove) return
chart.swapAxes(axisAbove, axisY)
}
const moveDownButton = document.createElement('button')
headerDiv.appendChild(moveDownButton)
moveDownButton.textContent = 'Down'
moveDownButton.style.background = 'transparent'
moveDownButton.style.border = 'none'
moveDownButton.style.color = 'inherit'
moveDownButton.onclick = () => {
const axes = chart.getAxes(AxisPosition.Left)
const i = axes.indexOf(axisY)
const axisBelow = axes[i - 1]
if (!axisBelow) return
chart.swapAxes(axisBelow, axisY)
}
const deleteButton = document.createElement('button')
headerDiv.appendChild(deleteButton)
deleteButton.textContent = 'Delete'
deleteButton.style.background = 'transparent'
deleteButton.style.border = 'none'
deleteButton.style.color = 'inherit'
deleteButton.onclick = () => {
axisY.dispose()
const i = channels.indexOf(ch)
channels.splice(i, 1)
headerDiv.remove()
}
// Positioning logic between Chart with stacked axes and HTML overlays.
// 1) Positioning of HTML is done using LCJS events that inform when axis layout changes.
const handleLayoutChange = (event) => {
const position = event.axes.get(axisY)
if (!position) return
headerDiv.style.top = `${position.top}px`
headerDiv.style.transform = 'translateY(-100%)'
// 2) Allocate space between Y axes of the chart, based on the size of the HTML UI.
axisY.setMargins(0, headerDiv.getBoundingClientRect().height)
event.userChangedLayout()
}
chart.addEventListener('layoutchange', handleLayoutChange)
// Automatically destroy chart event listener if axis (channel) is removed to not keep references.
axisY.addEventListener('dispose', () => chart.removeEventListener('layoutchange', handleLayoutChange))
const ch = { ...info, axisY, series }
return ch
}
channels = channels.map(createChannel)
const addChButton = document.createElement('button')
chart.engine.container.append(addChButton)
addChButton.textContent = 'Add channel'
addChButton.style.background = 'transparent'
addChButton.style.border = 'none'
addChButton.style.color = 'inherit'
addChButton.style.position = 'absolute'
addChButton.style.bottom = '0px'
addChButton.style.left = '0px'
addChButton.onclick = () => {
channels.push(createChannel({ name: `Channel ${channels.length + 1}` }))
}
chart.engine.container.style.color = isDarkTheme ? 'white' : 'black'
// Sticky X axis
// A second ChartXY containing only an X-axis acts as the sticky overlay.
const stickyXChart = lc
.ChartXY({
container: stickyXContainer,
legend: { visible: false },
})
.setTitle('')
.setBackgroundFillStyle(backgroundFill)
.setSeriesBackgroundFillStyle(backgroundFill)
.setSeriesBackgroundStrokeStyle(emptyLine)
stickyXChart.axisY.dispose()
synchronizeAxisIntervals(chart.axisX, stickyXChart.axisX)
stickyXChart.axisX.setTickStrategy(AxisTickStrategies.DateTime)
// Match the sticky overlay with the real X-axis layout.
chart.addEventListener('layoutchange', (event) => {
const axisLayout = event.axes.get(chart.axisX)
if (!axisLayout) return
const height = Math.ceil(event.chartHeight - axisLayout.top)
if (stickyXContainer.clientHeight !== height) {
stickyXContainer.style.height = `${height}px`
}
if (stickyXContainer.clientWidth !== event.chartWidth) {
stickyXContainer.style.width = `${event.chartWidth}px`
}
stickyXChart.setPadding({
left: event.margins.left,
right: event.margins.right,
})
stickyXChart.engine.layout()
})
// Show the overlay until the real X-axis enters the viewport.
const updateStickyAxis = () => {
const distanceFromBottom = scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight
stickyXContainer.style.visibility = distanceFromBottom > 1 ? 'visible' : 'hidden'
}
scrollContainer.addEventListener('scroll', updateStickyAxis)
updateStickyAxis()
JavaScript Multi Channel Layout Dashboard - Editor Example of commonly required layout with several channels, shared time axis and HTML interoperation.
The use case is:
Multiple channels each on own Y axis (different units, measurements, value ranges) Shared time view (X axis) Requirement to have user interface controls for every channel, in this example there are:Move up button Move down button Delete channel button Channels can be dynamically added or removed Many charts which can be scrolled up/down in view. Shared time view stays always visible (Sticky X axis) For sticky X-axis specific guidance, prefer the dedicated example: https://lightningchart.com/js-charts/interactive-examples/examples/lcjs-example-0057-stickyXAxis.html
The example shows how it is possible to creatively interoperate with normal HTML rendered content along side the charts even with this kind of complicated layout. In this example, the HTML content is created with JavaScript APIs, but in real applications likely a framework such as React, Angular or Vue would be used to render this content. Even so, the same logic can be applied.
The base idea is that HTML content is positioned based on ChartXY layout which is reported using layoutchange event. Afterwards, the ChartXY layout is adjusted to make space for the HTML content depending on its height.
Sounds complicated, but really its just these few lines of code:
const handleLayoutChange = ( event ) => {
const position = event. axes. get ( axisY)
if ( ! position) return
headerDiv. style. bottom = ` ${ position. top} px `
axisY. setMargins ( 0 , headerDiv. getBoundingClientRect ( ) . height)
event. userChangedLayout ( )
} This is made possible by the convenience of layoutchange event, which supplies very detailed information about the positioning of axes and other elements of the chart and is fired automatically whenever this layout changes.