Axis
Here you can find guides for most often required configurations of Axis, a critical part of almost all chart types.
Accessing axis
This depends on the type of chart.
const axisX = ChartXY.axisX
const axisY = ChartXY.axisY
const axisZ = Chart3D.axisZ
const radialAxis = PolarChart.radialAxis
const amplitudeAxis = PolarChart.amplitudeAxis
const valueAxis = BarChart.valueAxis
const categoryAxis = BarChart.categoryAxis
Axis title
axis
.setTitle('Voltage')
.setTitleFont(font => font.setSize(10).setFamily('Segoe UI'))
.setTitleFillStyle(new SolidFill({ color: ColorRGBA(255, 0, 0) }))
For ChartXY, setTitlePosition and setTitleRotation are also available. See API documentation for more details.
For more details about style API, please see Styles, colors and fonts.
Axis interval / view
By default, any axis automatically adjusts its "interval" [start - end] to display all series connected to it.
The start-end interval can be explicitly set using the setInterval or setDefaultInterval methods.
// Example, show interval between 0 and 100
axis.setInterval({ start: 0, end: 100 })
By using setDefaultInterval this configured interval is also restored if the user triggers the "zoom to fit" interaction (double click chart area by default).
axis.setDefaultInterval({ start: 0, end: 100 })
Numeric axis
Display numeric values using automatically placed axis ticks (10, 20, 30, etc.).
This is the default axis behavior.
axis.setTickStrategy(AxisTickStrategies.Numeric)


Extreme ticks
By default, ticks are placed on logical key values (e.g. 0, 10, 20, 30, ...) depending on active axis interval. Sometimes, with physically small charts you may get problems from not having enough ticks visible. One solution for this is to enable "extreme ticks", this simply means always displaying the axis start and end values, even if they are not "logical key values".
axis.setTickStrategy(AxisTickStrategies.Numeric, (ticks) => ticks
// Enable extreme ticks
.setExtremeTickStyle(ticks.getMajorTickStyle())
)
Even if extreme ticks are not enabled, in some cases they can be displayed as an automatic fallback if otherwise a small Numeric axis would only display 1 tick label. This behavior can be prevented with:
axis.setTickStrategy(AxisTickStrategies.Numeric, (strategy) => strategy
.setFallBackToExtremeTicksAutomatically(false)
)
Date-Time axis
Display seconds, minutes, hours, days, months and years using automatically placed axis ticks.
axis.setTickStrategy(AxisTickStrategies.DateTime)


Date-time axis intervals are defined as UTC timestamps, meaning timestamp = milliseconds since 1st January 1970.
axis.setInterval({
start: Date.UTC(2022, 0, 1), // 1st Jan 2022
end: Date.UTC(2022, 0, 31) // 31st Jan 2022
})
Zoom ability
By default, you can't zoom in closer than around 1 day range. To improve on this, you can enable so called "high precision axis", which enables zooming to 1 millisecond range:
const chart = lc.ChartXY({
defaultAxisX: {
type: 'linear-highPrecision'
}
})
If you need to display even smaller time steps, microseconds or nanoseconds, see Nanosecond timestamps
Show year / month / day
In some use cases you might want to have an additional label displaying for example what year or month is in question. One way of doing this is by enabling great ticks:
chart.axisX.setTickStrategy(AxisTickStrategies.DateTime, (strategy) =>
strategy.setGreatTickStyle(strategy.getMajorTickStyle().setTickLength(28).setTickStyle(emptyLine)),
)
These are automatically displayed on relevant zoom levels with just one (or two) extra labels so you don't have to display year/month/day on every tick label.
Custom formatting
Commonly used alternate formatting scheme of formatting every tick with "HH:MM:SS:MMM":
const t1s = 1000
const t1m = 60 * 1000
const t1h = 60 * 60 * 1000
const t1d = 24 * 60 * 60 * 1000
const formatterX = (x) => {
const hour = Math.floor((x % t1d) / t1h).toString().padStart(2, '0')
const minute = Math.floor((x % t1h) / t1m).toString().padStart(2, '0')
const second = Math.floor((x % t1m) / t1s).toString().padStart(2, '0')
const millis = Math.floor((x % t1s)).toString().padStart(3, '0')
return `${hour}:${minute}:${second}.${millis}`
}
chart.axisX.setTickStrategy(AxisTickStrategies.DateTime, strategy => strategy
.setFormatting(formatterX, formatterX, formatterX)
.setCursorFormatter(formatterX)
)
Different formatting schemes can be applied by simply acting differently in the formatter logic based on current Axis interval (Axis.getInterval()) OR separately defining formatter functions for different zoom levels (setFormattingDay, setFormattingHour, etc.).
Modifying default formatting without full override
Built-in date-time formatting is based on Intl.DateTimeFormat, so it is possible to add global overrides - for example, adding timeZone: "GMT" to all zoom levels:
chart.axisX.setTickStrategy(AxisTickStrategies.DateTime, strategy => {
const keys = Object.keys(strategy.toJS())
return strategy.withMutations((mutable) => {
mutable.set('utc', true) // align ticks by GMT+0 rather than client timezone
mutable.set('cursorFormatter', (x) => new Intl.DateTimeFormat(undefined, { timeZone: 'GMT', hour: '2-digit'}).format(x))
keys.forEach((key) => {
if (!key.includes('formatOptions')) return
const prevValue = strategy[key]
if (typeof prevValue === 'function') return
mutable.set(key, { ...prevValue, timeZone: 'GMT' })
})
})
})
Time axis
Display passing of time since a reference point using automatically placed axis ticks.
axis.setTickStrategy(AxisTickStrategies.Time)


Time axis intervals are defined as milliseconds.
// Example, show range of 10 seconds
axis.setInterval({ start: 0, end: 10_000 })
Time axis is well suited to display "run-time", as in how long the application has been running:
setInterval(() => {
lineSeries.appendSample({ x: performance.now(), y: Math.random() })
}, 1000 / 60)
Alternatively, you can input UTC timestamps and specify which timestamp is displayed as 00:00:00:
axis.setTickStrategy(AxisTickStrategies.Time, (ticks) => ticks.setTimeOrigin(-Date.now()))
setInterval(() => {
lineSeries.appendSample({ x: Date.now(), y: Math.random() })
}, 1000 / 60)
Custom formatting
const formatMilliseconds = (ms) => {
const seconds = Math.floor(ms / 1000);
const milliseconds = Math.round(ms % 1000);
return `${seconds.toString().padStart(2, '0')}:${milliseconds.toString().padStart(3, '0')}`;
}
chart.axisX.setTickStrategy(AxisTickStrategies.Time, strategy => strategy
.setFormattingFunction((x, range) => formatMilliseconds(x))
)
Custom formatting can be adjusted to active zoom level by referring to supplied range parameter.
If needed, cursor formatting can be separately configured with setCursorFormatter method.
Disable ticks
axis.setTickStrategy(AxisTickStrategies.Empty)
Categorical axis
The recommended way to realize categorical axis is found below:
const categories = ['A', 'B', 'C', 'D', 'E']
chart.axisY
.setDefaultInterval({ start: 0, end: categories.length - 1 })
.setTickStrategy(AxisTickStrategies.Numeric, (strategy) =>
strategy
.setCustomTickPlacement(() => categories.map((_, i) => ({ position: i })))
.setFormattingFunction((pos) => categories[Math.round(pos)] ?? '')
.setCursorFormatter((pos) => categories[Math.round(pos)] ?? ''),
)
Y values would then correspond to categories like:
- 0 => 'A'
- 1 => 'B'
- ...
Custom tick placement logic
If the different available built-in tick placement logics (numeric, time, datetime) don't suit your use case, you can override with your own. This logic can be done:
- For 1 tick level only (major ticks generally). In this case, minor ticks or other tick levels are not shown.
- For each tick level separately (i.e. major & minor)
// Example, always show exactly 10 gridlines
chart.axisY.setTickStrategy(AxisTickStrategies.Numeric, (strategy) =>
strategy.setCustomTickPlacement((info) =>
new Array(10 + 1)
.fill(0)
.map((_, i) => ({ position: info.interval.start + (i / 10) * (info.interval.end - info.interval.start) })),
),
)
// Example, always show exactly 10 major gridlines and 5 minor gridlines between each
chart.axisY.setTickStrategy(AxisTickStrategies.Numeric, (strategy) =>
strategy.setCustomTickPlacement({
major: (info) =>
new Array(10 + 1)
.fill(0)
.map((_, i) => ({ position: info.interval.start + (i / 10) * (info.interval.end - info.interval.start) })),
minor: (info) => {
const majorStep = (info.interval.end - info.interval.start) / 10
const minorStep = majorStep / 5
const minorTicks: CustomTickPlacementResult[] = []
let step = 0
while (true) {
const position = info.interval.start + step * minorStep
if (Math.sign(info.interval.end - position) !== Math.sign(info.interval.end - info.interval.start)) break
if (step % 5 === 0) {
// Dont add minor tick here, would overlap with major tick
} else {
minorTicks.push({ position })
}
step++
}
return minorTicks
},
}),
)
Custom ticks
Independent, single axis ticks can be placed at any location by creating a "custom tick" object:
const tick = chart.axisX.addCustomTick()
.setValue(10)
.setTextFormatter(_ => `Hello`)
.setMarker(marker => marker
.setTextFont(font => font.setWeight('bold'))
.setTextFillStyle(new SolidFill({ color: ColorRGBA(255, 0, 0) }))
)
Custom ticks are available currently in XY, 3D and Parallel coordinate charts.
In XY charts you can also show a different looking custom tick, so called "Pointable text box":
const tick = chart.axisX.addCustomTick(UIElementBuilders.PointableTextBox)
.setValue(10)
.setTextFormatter(_ => `Hello`)
.setMarker(marker => marker
.setTextFont(font => font.setWeight('bold'))
.setTextFillStyle(new SolidFill({ color: ColorRGBA(255, 0, 0) }))
)
Scrolling axis
// Example, configure a scrolling axis that keeps interval with length: `10_000`
chart.axisX
.setScrollStrategy(AxisScrollStrategies.scrolling)
.setDefaultInterval((state) => ({
end: state.dataMax ?? 0,
start: (state.dataMax ?? 0) - 10_000,
stopAxisAfter: false,
}))
This results in the axis automatically revealing new data as it is spawned, but keeping the size of axis interval unchanged - resulting in older data going out of view.
Axes can be configured to scroll in any direction. See Scrolling to different directions for more details.
Real-time scrolling axis
Often data arrives in applications in batches, say every 1 second, even though data points are monitored 100s per second. This is generally done to optimize data transfers.
For this kind of use cases, scrolling automatically as time passed on should be enabled for a smoother display of incoming data:
chart.axisX
.setScrollStrategy(AxisScrollStrategies.scrolling({
realTime: true // (!)
}))
.setDefaultInterval((state) => ({
end: state.dataMax ?? 0,
start: (state.dataMax ?? 0) - 10_000,
stopAxisAfter: false,
}))
By default, real time scrolling is configured for data points arriving around once every second. If data points arrive significantly more infrequently (e.g. every 5 seconds, 10 seconds, 1 minute, ...) then you should configure catch up threshold:
axis.setScrollStrategy(AxisScrollStrategies.scrolling({
realTime: { catchUpThresholdMs: 10_000 } // (!)
}))
.setDefaultInterval((state) => ({
end: state.dataMax ?? 0,
start: (state.dataMax ?? 0) - 10_000,
stopAxisAfter: false,
}))
This value is required to cope with situations where data flow is interrupted, and later continues, so that axis can "catch up" to live data. Alternatively, catching up can be disabled by setting the threshold value to an extremely large number.
In cases where data flow may not be 100% stable, it can be useful to configure the time axis to scroll slightly behind real-time so that latency variations are not displayed in any way:
axis.setScrollStrategy(AxisScrollStrategies.scrolling({
realTime: { catchUpThresholdMs: 10_000 }
}))
.setDefaultInterval((state) => ({
end: (state.dataMax ?? 0) - 2_000, // (!)
start: (state.dataMax ?? 0) - 12_000, // (!)
stopAxisAfter: false,
}))
Axis fitting
By default, axes set their view to contain all data in visible range. e.g. imagine case with time X axis, and some value Y axis, the Y axis will adjust to show min-max range of all data in the visible time range.
The default axis behavior is to fit against visible data only, but actually only PointLineAreaSeries supports this. With other series, the behavior is always equal to considerVisibleRangeOnly: false
Fitting to whole data set rather than only visible data
Recommended in use cases where the visible range of data fluctuates a lot, or for data exploration applications if user prefers that the Y axis remains static during zooming in/out etc.
chart.axisY.setScrollStrategy(
AxisScrollStrategies.fitting({
considerVisibleRangeOnly: false,
}),
)
Fitting only one side of axis
// Example, keep axis `start` always at 0 while fitting `end` side against visible data.
chart.axisY
.setScrollStrategy(AxisScrollStrategies.fitting({ start: false }))
.setInterval({ start: 0, end: 1, stopAxisAfter: false })
Controlling spacing around edges of axis
By default, automatic scroll margins are enabled, which means the actual value is based on attached series and their style.
For example, in case of point series, the scroll margins are set to half of point size. This can be controlled with setScrollMargins method:
// Example, 20 pixels of empty space in upper series area
chart.axisY.setScrollMargins({ start: 0, end: 20 })
In this context, scrolling refers to any automatic axis adjusting to attached series.
If axis interval is explicitly set with setInterval or setDefaultInterval, then scroll margins are not applied.
Axis units
When you know what Units your axis represents you can utilize Axis.setUnits(unit: string), which might make your application development a little bit easier.
chart.axisY
.setTitle('Frequency')
.setUnits('Hz')
Setting the Units only affects following things:
- The unit is displayed after axis title (if specified) - e.g. "Frequency (Hz)"
- The unit is displayed by default cursor formatters when pointing at series connected to that axis - e.g. "Y: 18.2 Hz"
The main value in using Axis.setUnits is to avoid having to specify custom cursor formatting just because you need to display the units in there.
These behaviors can also be individually control like so:
chart.axisY.setUnits('Hz', {
displayInCursor: true,
displayOnAxis: true
})
Multiple axes and positioning
In ChartXY, you can have more than two axes, and axes can also be positioned in different locations.
Additional axes can be added with addAxisX and addAxisY methods:
const axisX2 = chart.addAxisX().setTitle('Extra Axis X')
By default, series are attached to the default axes. When an application has several axes, it is recommended to explicitly specify which axis each series should be connected to:
const lineSeries = chart.addLineSeries({
xAxis: axisX2,
yAxis: chart.axisY
})
Axis side
Axes can be positioned at the opposite end of the chart (right for X axis, top for Y axis):
const axisY2 = chart.addAxisY({ opposite: true })


For default axes, this has to be specified when the ChartXY is created:
const chart = lc.ChartXY({
defaultAxisY: { opposite: true }
})
Stacked axes
Multiple axes can also be stacked on top of each other. Most commonly this is used to share 1 X axis between many different Y axes and respective Series:
chart.axisY.dispose()
for (let i = 0; i < 3; i += 1) {
const axisY = chart.addAxisY({ iStack: i }).setMargins(i > 0 ? 15 : 0, i < 2 ? 15 : 0)
const series = chart
.addLineSeries({ axisY })
.appendSamples({ yValues: new Array(100).fill(0).map((_) => Math.random()) })
}


The key parts here are:
iStackparameter given toaddAxisYmethod. This determines the vertical position of the Y axis.Axis.setMarginsis used to add empty space along vertical plane to avoid Y axis tick labels colliding, and adding a clear separation between channels.- For simplicity of implementation, the default Y axis is disposed before creating the stacked Y axes.
The length of axes can be changed in two different ways:
// Set relative length of axis to `0.5`
// By default, every axis has relative length of `1`, so this operation would effectively make the axis half the length of others
axis.setLength({ relative: 0.5 })
// Set length of axis to 200 pixels exactly.
axis.setLength({ pixels: 200 })
Parallel axes
Axes can also be placed parallel to each other:
const axisY1 = chart.axisY
const axisY2 = chart.addAxisY({ iParallel: 1 })
.setTickStrategy(AxisTickStrategies.Numeric, (ticks) => ticks.setTickStyle((major) => major.setGridStrokeStyle(emptyLine)))
const series1 = chart.addLineSeries({ axisY: axisY1 })
.appendSamples({ yValues: new Array(100).fill(0).map((_) => Math.random()) })
const series2 = chart.addLineSeries({ axisY: axisY2 })
.appendSamples({ yValues: new Array(100).fill(0).map((_) => Math.random()) })


Generally, when there are parallel axis, you want to disable tick gridlines because it is practically impossible to recognize which axis they belong to. In above example, one of the Y axis has gridlines visible and the other not.
Parallel axes can also be combined with opposite axes, so that one Y axis is on left, and another on right.
User interactions
Currently most axis types have no user interactions, except for those belonging to ChartXY
Generally, the best way to configure user interactions of axes is to configure them via the owning chart. Please see Chart XY user interactions section for more information.
However, further user interaction overrides can be configured on any individual axis. This essentially allows:
- Controlling whether the particular axis is affected by chart level interactions which by default affect all axes.
- Controlling what user interactions are enabled above the particular axis - overriding the chart level configuration.
axis.setUserInteractions({
chartInteractions: false // this axis will be unaffected by any and all "chart level" interactions
})
axis.setUserInteractions({
chartInteractions: {
pan: false, // alternatively, individual "chart level" interactions can be disabled like so
zoom: false
}
})
Styling axis ticks
Axis ticks are configured via the AxisTickStrategy interface:
axis.setTickStrategy(AxisTickStrategies.Numeric, tickStrategy => tickStrategy
.setTickStyle(ticks => ticks
.setLabelFillStyle(new SolidFill({ color: ColorRGBA(255, 0, 0) }))
.setLabelFont(font => font.setSize(10).setFamily('Segoe UI'))
.setGridStrokeStyle(new SolidLine({ thickness: 1, fillStyle: new SolidFill({ color: ColorRGBA(255, 0, 0) }) }))
.setTickStyle(emptyLine)
)
)
Alternatively instead of setTickStyle, you can style major and minor ticks separately using setMajorTickStyle / setMinorTickStyle.
For more details about style API, please see Styles, colors and fonts.
Same syntax works for DateTime and Time axis tick strategies.
Styling axis line
// Hide axis line
axis.setStrokeStyle(emptyLine)
// Red axis line
axis.setStrokeStyle(new SolidLine({ thickness: 1, fillStyle: new SolidFill({ color: ColorRGBA(255, 0, 0) }) }))
For more details about style API, please see Styles, colors and fonts.
Hiding grid-lines
axis.setTickStrategy(AxisTickStrategies.Numeric, tickStrategy => tickStrategy
.setTickStyle(ticks => ticks
.setGridStrokeStyle(emptyLine)
)
)
Same syntax works for DateTime and Time axis tick strategies.
Zebra stripes
Colored bands can be enabled on any axis to help visually distinguish regions along the axis.
The step parameter defines the width of each stripe in axis units:
// Numeric axis - stripes every 1000 axis units
axis.setZebraStripes({
getLayout: () => ({ step: 1000 })
})
// DateTime axis - daily stripes
axis.setZebraStripes({
getLayout: () => ({ step: 24 * 60 * 60 * 1000 }) // 1 day in milliseconds
})
By default, stripes start from axis value of 0. This can be changed with the start parameter:
const oneDay = 24 * 60 * 60 * 1000 // 1 day in milliseconds
// Daily stripes starting at a specific date
axis.setZebraStripes({
getLayout: () => ({ step: oneDay, start: Date.UTC(2026, 0, 1) }),
})
// Daily stripes aligned to local timezone midnight
// With DateTime axis, stripes anchor to Unix epoch (0 = midnight UTC).
// Use timezone offset to align with local calendar days:
axis.setZebraStripes({
getLayout: () => ({
step: oneDay,
start: new Date().getTimezoneOffset() * 60 * 1000
}),
})
By default, only odd-numbered stripes are drawn. Use color1 to change the default color, and color2 to enable drawing even-numbered stripes:
axis.setZebraStripes({
getLayout: () => ({ step: 1000 }),
color1: ColorRGBA(240, 240, 240),
color2: ColorRGBA(220, 220, 220)
})
The getLayout callback receives information about current axis state, which can be used to create dynamic stripe layouts:
axis.setZebraStripes({
getLayout: (info) => {
const interval = info.end - info.start
const oneDay = 24 * 60 * 60 * 1000
if (interval <= 7 * oneDay) {
// Week or less: daily stripes
return { step: oneDay }
}
// More than a week: weekly stripes
return { step: 7 * oneDay }
}
})
Logarithmic axis
Logarithmic axes can be enabled when creating an Axis or Chart. Here's how it looks for ChartXY:
const chart = lc.ChartXY({
defaultAxisY: {
type: 'logarithmic',
base: 10,
}
})
...or for non-default Axis:
const axisLog = chart.addAxisY({ type: 'logarithmic', base: 10 })
Please note that logarithmic axis range is not defined at 0, so you should confirm that your data doesn't have exact 0 values.
Inverted or reverted axis
By default, axis fits visible data range so that min value is shown at axis "start" (left for X axis, bottom for Y axis). This can be reversed so that min value is shown at axis "end" (right for X axis, top for Y axis) like this:
// Reverse axis interval
axis.setInterval({ start: 1, end: 0, stopAxisAfter: false })
Restricting axis interval
// Example 1, prevent zooming outside active data set
axis.setIntervalRestrictions((state) => ({
startMin: state.dataMin,
endMax: state.dataMax,
}))
// Example 2, set max zoom in level (intervalMin)
axis.setIntervalRestrictions({ intervalMin: 10 })
// Example 3, set max zoom out level (intervalMax)
axis.setIntervalRestrictions({ intervalMax: 1000 })
// Example 4, disable any restrictions (by default axis are restricted to loaded data set)
chart.forEachAxis(axis => axis.setIntervalRestrictions(undefined))
Restrict zoom level to certain decimal count
While there is no convenience feature for restricting axis by decimal count, it is relatively easy to achieve with setIntervalRestrictions by manually identifying the thresholds where built-in tick formatting changes number of decimals.
// Some examples
axis.setIntervalRestrictions({ intervalMin: 100 }) // max decimals = 0
axis.setIntervalRestrictions({ intervalMin: 10 }) // max decimals = 1
axis.setIntervalRestrictions({ intervalMin: 1 }) // max decimals = 2
Following changes to axis interval
axis.addEventListener('intervalchange', (event) => {
console.log(event)
})
Synchronizing several axes
Any number of axes can be conveniently synchronized with synchronizeAxisIntervals function.
This ensures that their interval is always the same.
synchronizeAxisIntervals(axis1, axis2, axis3)
It's worth pointing out that this operation creates a lasting connection between the axes. If any axis should be removed at any time, then the synchronization should be cleaned up:
const syncHandle = synchronizeAxisIntervals(axis1, axis2, axis3)
syncHandle.remove()
Syncing axes affects only axis intervals and stopped state. Scroll margins can't be synchronized in this way, but it can be done manually:
axis1.setScrollMargins(5)
axis2.setScrollMargins(5)
axis3.setScrollMargins(5)
Removing series from affecting axis
Individual series can be disabled from affecting any axis scrolling or fitting using setAutoScrollingEnabled method:
series.setAutoScrollingEnabled(false)
Zoom range limitations
LightningChart JS is based on WebGL powered graphics. This imposes some limitations when dealing with extremely large numbers, or numbers with extremely precise decimal points.
The main way this limitation shows itself is Axis zoom range limitations, meaning axis preventing its interval [start: number, end: number] from going to ranges that could result in rendering errors.
If your application results in Axis refusing to go to required zoom range, you should change the axis type to "high precision":
const chart = lightningChart().ChartXY({
defaultAxisX: {
type: 'linear-highPrecision'
}
})