Close Editor Run Reset Auto Update CJS const lcjs = require('@lightningchart/lcjs')
const {
lightningChart,
Themes,
AxisTickStrategies,
emptyFill,
emptyLine,
defaultAxisIntervalRestrictionsXY,
SolidFill,
SolidLine,
ColorRGBA,
ImageFill,
ImageFitMode,
PalettedFill,
LUT,
ColorPalettes
} = lcjs
// Initialize LightningChart JS
const lc = lightningChart()
const exampleContainer = document.getElementById('chart') || document.body
if (exampleContainer === document.body) {
exampleContainer.style.width = '100vw'
exampleContainer.style.height = '100vh'
exampleContainer.style.margin = '0px'
}
const containerChart1 = document.createElement('div')
exampleContainer.append(containerChart1)
const chart = lc.ChartXY({
container: containerChart1,
legend: {
marginInner: 0,
marginOuter: 5,
padding: 0,
entries: {buttonFillStyle: emptyFill},
visible: false
},
// theme: Themes.darkGold
})
.setTitle('Price Distribution of Diamonds by Carat')
chart.forEachAxis(axis => axis.setIntervalRestrictions(defaultAxisIntervalRestrictionsXY))
containerChart1.style.width = '100%'
containerChart1.style.height = '100%'
containerChart1.style.position = 'relative'
containerChart1.style.overflow = 'hidden'
const series = chart.addPointSeries({
schema: {
carat: { pattern: null },
price: { pattern: null },
clarity_rank: { pattern: null }
}
})
.setName('Clarity')
.setPointSize(2)
.setPointStrokeStyle(emptyLine)
fetch(document.head.baseURI + 'examples/assets/1711/diamonds.json')
.then((r) => r.json())
.then((data) => {
const clarityLookup = {}
data.forEach(item => {
clarityLookup[item.clarity_rank] = item.clarity
})
const colors = [0, 1, 2, 3, 4, 5, 6, 7].map(ColorPalettes.warm(8))
const lutSteps = Object.keys(clarityLookup)
.map(rank => Number(rank))
.sort((a, b) => a - b)
.map(rank => ({
value: rank,
color: colors[rank - 1],
label: clarityLookup[rank]
}))
series.setPointFillStyle(new PalettedFill({
lookUpProperty: 'value',
lut: new LUT({
interpolate: false,
steps: lutSteps
})
}))
series.setDataMapping({ x: 'carat', y: 'price', lookupValue: 'clarity_rank'})
series.appendJSON(data, { whitelist: ['carat', 'price', 'clarity_rank'] })
chart.axisX.setTitle('Carat')
chart.axisY.setTitle('Price ($)')
// Set startup zoom intervals
chart.axisX.setInterval({start: 0.34, end: 3.17})
chart.axisY.setInterval({start: 1200, end: 12000})
const maxInterval = series.getXMax() - series.getXMin()
chart.setCursorFormatting((_, hit) => {
const clarity = clarityLookup[hit.lookupValue];
return [
['Carat', '', hit.x.toFixed(2)],
['Price ($)', '', hit.y.toFixed(0)],
['Clarity', '', clarity]
]
})
// Create second chart overlaid above the first one that is used to control zoom range on the main chart
const overlayContainer = document.createElement('div')
exampleContainer.append(overlayContainer)
const overlayChart = lc.ChartXY({
container: overlayContainer,
legend: { visible: false },
// theme: Themes.darkGold
})
.setTitle('')
.setPadding(0)
.setCursorMode(undefined)
.setUserInteractions(undefined)
overlayContainer.style.position = 'absolute'
overlayContainer.style.zIndex = '1'
overlayChart
.setBackgroundFillStyle(emptyFill)
.setSeriesBackgroundFillStyle(new SolidFill({ color: ColorRGBA(0,0,0) }))
.setSeriesBackgroundStrokeStyle(new SolidLine({ thickness: 4, fillStyle: new SolidFill({ color: chart.getTheme().isDark ? ColorRGBA(200, 200, 200) : ColorRGBA(180, 180, 180) }) }))
overlayChart.engine.setBackgroundFillStyle(emptyFill)
overlayChart.forEachAxis(axis => axis.setThickness(0).setTickStrategy(AxisTickStrategies.Empty).setStrokeStyle(emptyLine))
const overlayRectangleS = overlayChart.addRectangleSeries()
.setAutoScrollingEnabled(false)
const overlayRectangle = overlayRectangleS.add({ x1: 0, x2: 0, y1: 0, y2: 0 })
.setFillStyle(new SolidFill({ color: ColorRGBA(0,0,0,0) }))
.setStrokeStyle(new SolidLine({ thickness: 1, fillStyle: new SolidFill({ color: chart.getTheme().isDark ? ColorRGBA(255, 252, 0) : ColorRGBA(0, 160, 28) }) }))
// Overlay charts axes should have always interval of full data set range
overlayChart.axisX.setInterval({ start: series.getXMin(), end: series.getXMax() })
overlayChart.axisY.setInterval({ start: series.getYMin(), end: series.getYMax() })
// Overlay charts rectangle follows main charts axis range and same other way
let axisSyncOngoing = false
chart.forEachAxis(axis => {
axis.addEventListener('intervalchange', () => {
if (axisSyncOngoing) return
axisSyncOngoing = true
// Adjust point size based on zoom level
const zoomInterval = (chart.axisX.getInterval().end - chart.axisX.getInterval().start) / maxInterval
const newPointSize = zoomInterval < 0.05 ? 6 :
zoomInterval < 0.2 ? 4 :
zoomInterval < 0.5 ? 3 : 2
series.setPointSize(newPointSize)
overlayRectangle.setDimensions({
x1: chart.axisX.getInterval().start,
x2: chart.axisX.getInterval().end,
y1: chart.axisY.getInterval().start,
y2: chart.axisY.getInterval().end
})
axisSyncOngoing = false
})
})
let debounceMoveOverlay
const syncMainChartFromOverlay = () => {
const dimensions = overlayRectangle.getDimensionsTwoPoints()
chart.axisX.setInterval({ start: dimensions.x1, end: dimensions.x2 })
chart.axisY.setInterval({ start: dimensions.y1, end: dimensions.y2 })
}
overlayRectangle.addEventListener('pointerdown', (eventDown) => {
let prev = eventDown
// Find out which part of the rectangle is being dragged
const coordStart = overlayChart.translateCoordinate(eventDown, overlayChart.coordsAxis)
const dimensionsStart = overlayRectangle.getDimensionsTwoPoints()
const calcDist = (x, y) => (coordStart.x - x) ** 2 + (coordStart.y - y) ** 2
const cornerDistances = {
lb: calcDist(dimensionsStart.x1, dimensionsStart.y1),
lt: calcDist(dimensionsStart.x1, dimensionsStart.y2),
rb: calcDist(dimensionsStart.x2, dimensionsStart.y1),
rt: calcDist(dimensionsStart.x2, dimensionsStart.y2),
center: calcDist((dimensionsStart.x1 + dimensionsStart.x2) / 2, (dimensionsStart.y1 + dimensionsStart.y2) / 2)
}
const corner = Object.entries(cornerDistances).reduce((prev, cur) => cur[1] < prev[1] ? cur : prev, ['center', 999999999999999])[0]
const handleMove = (eventMove) => {
const delta = {
x: eventMove.clientX - prev.clientX,
y: eventMove.clientY - prev.clientY
}
const dimensionsBefore = overlayRectangle.getDimensionsTwoPoints()
const pPixels = overlayChart.translateCoordinate({ x: dimensionsBefore.x1, y: dimensionsBefore.y1 }, overlayChart.coordsAxis, overlayChart.coordsRelative)
const pPixelsAfter = { x: pPixels.x + delta.x, y: pPixels.y - delta.y }
const pAxisAfter = overlayChart.translateCoordinate(pPixelsAfter, overlayChart.coordsRelative, overlayChart.coordsAxis)
const deltaAxis = { x: pAxisAfter.x - dimensionsBefore.x1, y: pAxisAfter.y - dimensionsBefore.y1 }
let dimensionsAfter
if (corner === 'center') {
dimensionsAfter = {
x1: pAxisAfter.x, y1: pAxisAfter.y,
x2: dimensionsBefore.x2 + (pAxisAfter.x - dimensionsBefore.x1),
y2: dimensionsBefore.y2 + (pAxisAfter.y - dimensionsBefore.y1),
}
} else if (corner === 'rt') {
dimensionsAfter = {
x1: dimensionsBefore.x1, y1: dimensionsBefore.y1,
x2: dimensionsBefore.x2 + deltaAxis.x, y2: dimensionsBefore.y2 + deltaAxis.y
}
} else if (corner === 'rb') {
dimensionsAfter = {
x1: dimensionsBefore.x1, y2: dimensionsBefore.y2,
x2: dimensionsBefore.x2 + deltaAxis.x, y1: dimensionsBefore.y1 + deltaAxis.y
}
} else if (corner === 'lb') {
dimensionsAfter = {
x2: dimensionsBefore.x2, y2: dimensionsBefore.y2,
x1: dimensionsBefore.x1 + deltaAxis.x, y1: dimensionsBefore.y1 + deltaAxis.y
}
} else if (corner === 'lt') {
dimensionsAfter = {
x2: dimensionsBefore.x2, y1: dimensionsBefore.y1,
x1: dimensionsBefore.x1 + deltaAxis.x, y2: dimensionsBefore.y2 + deltaAxis.y
}
}
if (!dimensionsAfter) return
// Prevent moving view outside min/max range
const rangeMaxX = overlayChart.axisX.getInterval()
const rangeMaxY = overlayChart.axisY.getInterval()
if (corner === 'center') {
if (dimensionsAfter.x2 > rangeMaxX.end) {
const diff = dimensionsAfter.x2 - rangeMaxX.end
dimensionsAfter.x1 -= diff
dimensionsAfter.x2 -= diff
}
if (dimensionsAfter.x1 < rangeMaxX.start) {
const diff = rangeMaxX.start - dimensionsAfter.x1
dimensionsAfter.x1 += diff
dimensionsAfter.x2 += diff
}
if (dimensionsAfter.y2 > rangeMaxY.end) {
const diff = dimensionsAfter.y2 - rangeMaxY.end
dimensionsAfter.y1 -= diff
dimensionsAfter.y2 -= diff
}
if (dimensionsAfter.y1 < rangeMaxY.start) {
const diff = rangeMaxY.start - dimensionsAfter.y1
dimensionsAfter.y1 += diff
dimensionsAfter.y2 += diff
}
} else {
if (dimensionsAfter.x2 !== dimensionsBefore.x2) {
dimensionsAfter.x2 = Math.max(dimensionsAfter.x2, dimensionsAfter.x1 + 0.05 * (rangeMaxX.end - rangeMaxX.start))
}
if (dimensionsAfter.y2 !== dimensionsBefore.y2) {
dimensionsAfter.y2 = Math.max(dimensionsAfter.y2, dimensionsAfter.y1 + 0.05 * (rangeMaxY.end - rangeMaxY.start))
}
if (dimensionsAfter.x1 !== dimensionsBefore.x1) {
dimensionsAfter.x1 = Math.min(dimensionsAfter.x1, dimensionsAfter.x2 - 0.05 * (rangeMaxX.end - rangeMaxX.start))
}
if (dimensionsAfter.y1 !== dimensionsBefore.y1) {
dimensionsAfter.y1 = Math.min(dimensionsAfter.y1, dimensionsAfter.y2 - 0.05 * (rangeMaxY.end - rangeMaxY.start))
}
dimensionsAfter.x1 = Math.max(dimensionsAfter.x1, rangeMaxX.start)
dimensionsAfter.x2 = Math.min(dimensionsAfter.x2, rangeMaxX.end)
dimensionsAfter.y1 = Math.max(dimensionsAfter.y1, rangeMaxY.start)
dimensionsAfter.y2 = Math.min(dimensionsAfter.y2, rangeMaxY.end)
}
overlayRectangle.setDimensions(dimensionsAfter)
if (debounceMoveOverlay) clearTimeout(debounceMoveOverlay)
debounceMoveOverlay = setTimeout(syncMainChartFromOverlay, 500) // 500ms debounce
prev = eventMove
}
const handleUp = (eventUp) => {
window.removeEventListener('pointermove', handleMove)
window.removeEventListener('pointerup', handleUp)
}
window.addEventListener('pointermove', handleMove)
window.addEventListener('pointerup', handleUp)
})
let isCapturing = false
let suppressLayoutChange = false
let debounceTimer
// Capture screenshot and set it as overlay background
const captureScreenshotToOverlay = () => {
if (isCapturing) return
isCapturing = true
suppressLayoutChange = true
const padding = chart.getPadding()
const title = chart.getTitle()
const cursorMode = chart.getCursorMode()
const xInterval = chart.axisX.getInterval()
const yInterval = chart.axisY.getInterval()
chart.setPadding(0)
chart.setTitle('')
chart.setCursorMode(undefined)
chart.legend.setOptions({ visible: false })
chart.axisX.setThickness(0).fit(false)
chart.axisY.setThickness(0).fit(false)
chart.engine.layout()
requestAnimationFrame(() => {
const screenshotDataUrl = chart.engine.captureFrame(undefined, undefined, true)
const img = new Image();
img.src = screenshotDataUrl
img.onload = () => {
overlayChart.setSeriesBackgroundFillStyle(new ImageFill({ source: img, fitMode: ImageFitMode.Stretch }))
chart.setPadding(padding)
chart.setTitle(title)
chart.setCursorMode(cursorMode)
chart.legend.setOptions({ visible: true })
chart.axisX.setThickness(undefined).setInterval(xInterval)
chart.axisY.setThickness(undefined).setInterval(yInterval)
chart.engine.layout()
suppressLayoutChange = false
setTimeout(() => { isCapturing = false }, 100)
}
})
}
// Sync overlay container size and position on main chart layout changes
chart.addEventListener('layoutchange', event => {
if (suppressLayoutChange) return
overlayContainer.style.left = `${event.margins.left}px`
overlayContainer.style.top = `${event.margins.top}px`
overlayContainer.style.width = `${event.viewportWidth * 0.15}px`
overlayContainer.style.height = `${event.viewportHeight * 0.15}px`
})
// Re-capture screenshot on resize
chart.engine.addEventListener('resize', () => {
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(captureScreenshotToOverlay, 200)
})
requestAnimationFrame(() => {
captureScreenshotToOverlay()
})
}) Scatter Chart with Integrated Zoom Overlay - Editor This example visualizes the relationship between price and carat weight across 53,940 diamonds. The data is categorized by clarity, using a color lookup table. Distinct vertical clusters at round-number weights (e.g., 1.0ct) show clear price jumps, marking where consumer demand for specific sizes creates market premiums.
An overview chart in the upper left corner displays the full dataset and indicates the current zoom level of the main chart. The zoom rectangle inside the overview chart can be resized and moved.
When the application launches, a screenshot of the main ChartXY is captured as a data URL and applied as the background fill for the overview chart. The background image updates automatically when the browser window is resized.
const screenshotDataUrl = chart. engine. captureFrame ( undefined , undefined , true )
const img = new Image ( ) ;
img. src = screenshotDataUrl
const fill = new ImageFill ( { source: img } )
overlayChart. setSeriesBackgroundFillStyle ( fill) The data used in this example: Tidyverse / Diamonds CSV .