Skip to main content
Version: 7.1.2

Line

The best and most appreciated feature of LightningChart JS is the simple LineSeries. This guide goes through most of the things you can do with it and how.

Chart with line seriesChart with line series

// Creation of a line series
const lineSeries = chart.addPointLineAreaSeries({ dataPattern: 'ProgressiveX' })
.setAreaFillStyle(emptyFill)
info

Line series are actually "Point line area series" with disabled area fill. This is mainly for performance reasons - this 1 class can be styled to many different use cases. More information why this is like so can be found here.

Adding data

There are many different ways you can add data to a Line series. The most basic way is to specify 2 number arrays, 1 for X coordinates, and another for Y coordinates. These arrays can be either primitive number arrays, or TypedArrays.

lineSeries.appendSamples({
xValues: [0, 1, 2],
yValues: [10, 12, 5]
})

Another common way of adding data is to read from a list of JavaScript objects:

lineSeries.appendJSON(
[
{ i: 0, voltage: 4.2 },
{ i: 1, voltage: 4.6 },
{ i: 2, voltage: 4.5 },
],
{ x: 'i', y: 'voltage' }
)

A common user problem is that data is supplied as string instead of number - please confirm this if you are experiencing any strange issues.

console.log(typeof y) // ---> 'number'

You can also omit either X or Y values for automatic incrementation:

lineSeries.appendSamples({
yValues: [10, 12, 5],
// Optional, defaults to 1
step: 2,
}) // ---> x: 0, 2, 4

Using timestamps

PointLineAreaSeries and DataSetXY APIs can directly consume timestamp data as any of following types:

warning

Features other than PointLineAreaSeries/DataSetXY currently only support direct input as UTC timestamp numbers!

// Example 1, date time string
lineSeries.appendSample({
x: '2022-12-31T22:00:00.000Z',
y: Math.random()
})
// Example 2, Date object
lineSeries.appendSample({
x: new Date(2023, 0, 1),
y: Math.random()
})
// Example 3, UTC timestamp number
setInterval(() => {
lineSeries.appendSample({
x: Date.now(),
y: Math.random(),
})
}, 100)

When using timestamps, you generally want to also setup a Date-Time Axis.

tip

Displaying nanosecond timestamps requires some extra configurations. See Nanosecond timestamps for more details.

Data cleaning / maximum memory use

In streaming use cases, memory usage can be limited by configuring the maximum number of samples that are retained. After this number is reached, the oldest samples will be dropped to keep only the max number of samples in memory.

// Example, keep max 1 million samples.
lineSeries.setMaxSampleCount(1_000_000)

Alternatively, if you are uncertain what value to use and don't want to allocate too much memory up-front, you can start small and automatically increase the buffer size as samples flow in:

lineSeries.setMaxSampleCount({ mode: 'auto', max: 10_000_000 })

This would first allocate only small amount of memory, progressively increase memory allocation as samples come in until eventually limiting sample count to 10 million.

Changing stroke color

const fillRed = new SolidFill({ color: ColorRGBA(255, 0, 0, 255) })
lineSeries.setStrokeStyle((stroke) => stroke.setFillStyle(fillRed))

Color can also be defined in a handful of other ways.

const fillBlue = new SolidFill({ color: ColorHEX('#0000ff') })
const fillGreen = new SolidFill({ color: ColorCSS('green') })

For more details about style API, please see Styles, colors and fonts.

Changing stroke thickness

lineSeries.setStrokeStyle((stroke) => stroke.setThickness(1))

By setting thickness to exactly -1 you can enable "primitive line rendering". This is very light on GPU utilization (works well on weak machines), but results in lines being 1 pixel thick and not very smooth.

// Enable primitive line rendering - 1 px thick and considerably lighter on GPU.
lineSeries.setStrokeStyle((stroke) => stroke.setThickness(-1))

Dashed line series

lineSeries.setStrokeStyle((stroke) => new DashedLine({
fillStyle: stroke.getFillStyle(),
thickness: stroke.getThickness(),
pattern: StipplePatterns.Dashed,
}))

Connecting to non-default axis

If your ChartXY has several axes, then you can specify which axis the series should be connected to like this:

const lineSeries = chart.addPointLineAreaSeries({
dataPattern: 'ProgressiveX',
xAxis: myXAxis,
yAxis: myYAxis,
})

Data gaps

If your data has gaps, which should not be connected in the line visualization, you can create a gap with a NaN Y value:

lineSeries.appendSamples({
yValues: [1, 2, NaN, 2, 1]
}) // ---> line from { x: 0, y: 1 } to { x: 1, y: 2 }, then GAP, starts again from { x: 3, y: 2 }.

Please note that the value has to be specifically NaN - undefined, null or any other value will not have the same effect.

Solve nearest from location

// Example 1, when user clicks on chart, log closest data point to console
chart.seriesBackground.addEventListener('click', event => {
const nearest = lineSeries.solveNearest(event)
console.log(nearest)
})
// Example 2, solve nearest data point from an axis coordinate
const locationAxis = { x: 10, y: 10 }
const locationClient = chart.translateCoordinate(locationAxis, chart.coordsAxis, chart.coordsClient)
const nearest = lineSeries.solveNearest(locationClient)
console.log(nearest)

Interactions with data points

All series support tracking user interactions:

lineSeries.addEventListener('click', (event, hit) => {
// hit.x
// hit.y
console.log(hit)
})

For all available events to track, please see API

Individual colors

const lineSeries = chart.addPointLineAreaSeries({
dataPattern: 'ProgressiveX',
colors: true
})
.setAreaFillStyle(emptyFill)
.setStrokeStyle((stroke) => stroke.setFillStyle(new IndividualPointFill()))
.appendSamples({
yValues: [10, 12, 9],
colors: [0xff0000ff, 0xff00ff00, 0xff00ff00], // red, green, green
})

Individual color line seriesIndividual color line series

Colors can also be supplied as Color objects created using one of the many Color factories (ColorRGBA, ColorHEX, etc.). However, for performance reasons you should avoid creating a large number of individual Color objects.

const colorRed = new SolidFill({ color: ColorRGBA(255, 0, 0) })
const colorGreen = new SolidFill({ color: ColorRGBA(0, 255, 0) })
lineSeries
.appendSamples({
yValues: [10, 12, 9],
// This is fine (reuse same color objects)
colors: [colorRed, colorGreen, colorGreen],
})

Color by lookup table

Alternative approach to individual colors, you can give each data point a number value, and specify rules for getting a color from any number:

const lineSeries = chart.addPointLineAreaSeries({
dataPattern: 'ProgressiveX',
lookupValues: true
})
.setAreaFillStyle(emptyFill)
.setStrokeStyle((stroke) => stroke.setFillStyle(new PalettedFill({
lookUpProperty: 'value',
lut: new LUT({
interpolate: true,
steps: [
{ value: 0, color: ColorCSS('red') },
{ value: 100, color: ColorCSS('green') },
]
})
})))
.appendSamples({
yValues: [10, 12, 9],
lookupValues: [0, 100, 50]
})
const legend = chart.addLegendBox().add(chart)

Lookup color line seriesLookup color line series

LUT has also a percentageValues option, which you can use if you want to use % values of the present min-max lookup value range, rather than specific value steps:

new LUT({
percentageValues: true,
interpolate: true,
steps: [
// 0 = 0 %, 1 = 100%
{ value: 0, color: ColorCSS('red') },
{ value: 1, color: ColorCSS('green') },
]
})

Color by X or Y

Instead of using lookup values in data set, you can also use X or Y coordinates directly, by changing the lookUpProperty value in PalettedFill.

new PalettedFill({
lookUpProperty: 'x', // 'x' or 'y'
lut: new LUT({
interpolate: true,
steps: [
{ value: 0, color: ColorCSS('red') },
{ value: 100, color: ColorCSS('green') },
]
})
})

Color by gradient

More alternatives to dynamic coloring - you can also define a gradient (linear or radial), to color the line stroke.

const lineSeries = chart.addPointLineAreaSeries({ dataPattern: 'ProgressiveX' })
.setAreaFillStyle(emptyFill)
.setStrokeStyle((stroke) => stroke.setFillStyle(
new LinearGradientFill({
// down -> up
angle: 0,
stops: [
{ offset: 0, color: ColorCSS('red') },
{ offset: 1, color: ColorCSS('green') },
],
})
))

Gradient color line seriesGradient color line series

Edit samples data

alterSamples

This method allows you to specify a starting location in the existing data set with a sample index. This is a simple counter that points to the n:th sample that has been pushed to the data set, in the order of appearance.

lineSeries.appendSamples({ yValues: [0, 1, 2, 3, 4, 5] })
lineSeries.alterSamples(1, { yValues: [10, 11] })
// ---> Data set = [0, 10, 11, 3, 4, 5]

alterSamples lets you modify any sample values, X, Y, lookup value, color, etc. It also supports automatically appending data if you attempt to modify samples that would come after the existing ones.

alterSamplesByID

This method allows you to arbitrarily edit any number of existing samples by identifying them with an user supplied ID property:

lineSeries.appendSamples({
yValues: [100, 101, 102, 103, 104],
ids: [0, 1, 2, 3, 4]
})
lineSeries.alterSamplesByID(
[2, 4], // Edit samples ID:2 and ID:4
{
yValues: [200, 201]
}
)
// ---> Data set yValues = [100, 101, 200, 103, 201]

IMPORTANT, in order to use ID values, you have to enable them when creating the Series or DataSetXY:

const series = chart.addPointLineAreaSeries({ ids: true })
// or
const dataSet = new DataSetXY({ ids: true })
// Example, load same value for all altered samples
lineSeries.alterSamplesByID([2, 4], { size: 5 })

fill

This method allows you to load a single value for all existing samples in the data set:

// Set point size of all samples to 5 pixels
lineSeries.fill({ size: 5 })

Read back data

Currently existing data in a PointLineAreaSeries can be read back using readback method:

lineSeries.readback()
// xValues: TypedArray
// yValues: TypedArray
// iSampleFirst: number
// lookupValues?: Float32Array | undefined
// colors?: Uint32Array | undefined
// ids?: Uint32Array | undefined
// sizes?: Float32Array | undefined
// rotations?: Float32Array | undefined

With progressive data sets you can also conveniently read back only data in given range:

series.readBack({ onlyInRange: chart.xAxis.getInterval() })
series.readBack({ onlyInRange: { start: 0, end: 100 } })