Constant-Velocity Real-Time Streaming in ApexCharts 6.0
Real-time charts have two hard parts: making the line scroll smoothly, and keeping memory flat while it runs for hours. ApexCharts 6.0 solves the second one for you. Set chart.streaming.enabled: true, keep appending points, and the library trims the series automatically to the visible window, so the data array never grows without bound. The scroll is constant-velocity by default, with no easing pulse. This post shows the new API, explains why appending (not shifting) is what actually scrolls a chart, and compares it to the manual trim you used to hand-write.
Key takeaways
chart.streaming.enabledauto-trims.appendDatatrims each series tomaxPoints, or to the visiblexaxis.rangewindow plus a runway, so memory stays flat.- Appending is what scrolls. Fixed points keep their x; the advancing axis moves their pixels left, so the line translates. Shifting a window each tick warps in place instead.
- The scroll is constant-velocity. Streaming keeps the window moving at an even speed with no easing pulse.
- It replaces manual trimming. Before 6.0 you trimmed the array yourself with a non-animated
updateSeries; that still works, but is no longer required. xaxis.rangeis a view setting, not a memory bound. It never removes data on its own.
See it live
The chart streams one point per second. Watch the two counters: appended climbs forever, while retained in memory stays bounded, because chart.streaming trimmed the off-screen points automatically. The window slides left at a steady pace.
chart.streaming.enabled trims the series to the visible window automatically, so “appended” climbs forever while “retained” stays bounded. The window slides left at constant velocity, no easing pulse and no in-place warp.
The streaming API
Enable streaming, set a window with xaxis.range, and match dynamicAnimation.speed to your tick interval. Then just append.
import ApexCharts from 'apexcharts'
const chart = new ApexCharts(document.querySelector('#chart'), {
series: [{ name: 'Signal', data: seed }],
chart: {
type: 'line',
streaming: { enabled: true }, // auto-trim + constant-velocity scroll
// streaming: { enabled: true, maxPoints: 100000 }, // or cap explicitly
animations: { dynamicAnimation: { speed: 1000 } }, // match the interval
zoom: { enabled: false },
},
xaxis: { type: 'datetime', range: 29_000 }, // 29-second visible window
yaxis: { min: 0, max: 100 },
})
await chart.render()
let lastX = Date.now()
setInterval(() => {
lastX += 1000
chart.appendData([{ data: [{ x: lastX, y: Math.round(Math.random() * 100) }] }])
}, 1000)
That is the whole thing. With streaming.enabled, you do not track the data yourself or trim it; the library bounds it to maxPoints when you set one, otherwise to the xaxis.range window plus a small runway.
Why appending scrolls (and shifting does not)
This is the mechanic worth understanding, because getting it wrong is the classic real-time bug.
appendData leaves every existing point where it is, at its index and its x, and adds the new point at the leading edge. As the axis max advances to that new point, every fixed point's pixel position moves left by exactly one step. The whole line translates. Keep the gap between points even (a steady cadence, or server timestamps at a fixed interval) and every scroll step is the same width.
The tempting alternative, dropping the oldest point and pushing a fixed-length window every tick, looks like it should scroll but does the opposite:
| Approach | What happens on the axis | What you see |
|---|---|---|
| Append (correct) | New point extends max; old points keep their x, pixels shift left | The line scrolls left smoothly |
| Shift a window each tick (wrong) | Axis rescales; index i maps to a different datum at the same pixel | Only y changes per point: the line warps in place |
So the rule is: append points, and let streaming (or an occasional trim) bound the array. Never rebuild a drop-oldest window every tick.
What changed from the pre-6 pattern
Before 6.0, xaxis.range set the visible window but never removed data, so a live chart's array grew forever unless you trimmed it yourself. The documented pattern was: append points, and periodically slice the array with a non-animated updateSeries (invisible, because the dropped points were off-screen).
// Pre-6 manual trim (still valid, still useful):
data.push(point)
if (data.length > 3 * VISIBLE) {
data = data.slice(-2 * VISIBLE)
chart.updateSeries([{ data }], false) // false => no animation, no flicker
}
Version 6 folds that into the library:
| Pre-6 (manual) | v6 (chart.streaming) | |
|---|---|---|
| Trimming | You slice the array + non-animated updateSeries | Automatic on appendData |
| Track data yourself | Yes | No |
| Constant-velocity scroll | You match cadence + speed carefully | Guaranteed by streaming |
| Memory bound | 2 * VISIBLE (your choice) | maxPoints, or the range window + runway |
The manual pattern is not deprecated; it is the right choice on older versions or when you want to decide exactly when and how much to trim. The real-time chart guide documents both, and our real-time dashboard tutorial builds a full multi-chart dashboard with the manual approach.
Wiring a WebSocket
A real feed replaces the interval with a message handler. The rules are the same, plus one: do not block the handler.
const socket = new WebSocket('wss://your-api.example.com/stream')
socket.addEventListener('message', (event) => {
const { t, value } = JSON.parse(event.data)
// fire-and-forget: do NOT await appendData in the handler
chart.appendData([{ data: [{ x: t, y: value }] }])
})
Use the server's timestamp as x so the scroll steps stay even, and do not await inside the handler: messages can arrive faster than render cycles, and awaiting would queue them up. For very high-rate feeds, buffer incoming values and append the newest batch on a timer or requestAnimationFrame, so the render rate is decoupled from the message rate.
Common mistakes
- Assuming
xaxis.rangebounds memory. It sets the view, not the data. Enablestreaming, setmaxPoints, or trim yourself. - Trimming a shifting window every tick. That warps the line in place instead of scrolling. Append; let streaming trim.
- Uneven point spacing. A jittery timer or a throttled background tab makes the scroll speed stutter. Use a steady cadence or fixed-interval server timestamps.
- Mismatched
dynamicAnimation.speed. If it does not match the interval, the chart idles between ticks or falls behind. Set them equal. - Not cleaning up. Clear the interval and close the socket on unmount, then
chart.destroy(), or updates target a dead instance.
Summary
ApexCharts 6.0 makes live charts a one-line concern: chart.streaming.enabled trims the series automatically as you appendData, so the window scrolls at constant velocity and memory stays flat for as long as the feed runs. The mechanic underneath is unchanged, appending points is what scrolls the line, and shifting a window is what warps it, but you no longer hand-write the trim. Set maxPoints or let it follow xaxis.range, match dynamicAnimation.speed to your cadence, and append. See the real-time chart guide and chart.streaming options for the reference, and the rest of what shipped in ApexCharts 6.0.
Frequently asked questions
What does chart.streaming do in ApexCharts 6.0?
chart.streaming.enabled turns on automatic memory management for live feeds. Once enabled, appendData() trims each series automatically, to maxPoints when set, otherwise to the visible xaxis.range window plus a small runway, so an always-on stream never grows the data array without limit. You no longer write your own periodic trim. The window also slides at constant velocity with no easing pulse.
Why does appending points scroll the chart instead of redrawing it?
appendData leaves every existing point at its index and x position and adds the new one at the leading edge. As the axis max advances to the new point, each fixed point's pixel moves left by one step, so the whole line translates smoothly. Dropping the oldest point and shifting a window every tick does the opposite: under the rescaled axis each point keeps its pixel and only its value changes, so the line warps in place instead of scrolling.
How is v6 streaming different from the old real-time pattern?
Before 6.0 you appended points and had to trim the series array yourself with an occasional non-animated updateSeries, because xaxis.range only sets the visible window and never removes data. In 6.0, chart.streaming.enabled does that trim for you and guarantees a constant-velocity scroll. The manual pattern still works and is the right choice on older versions or when you want full control over trimming.
Does xaxis.range bound memory on its own?
No. xaxis.range only computes the visible axis min and max; it never removes points from the series. Without chart.streaming.enabled or a manual trim, appendData grows the underlying array for as long as the stream runs. Enable streaming, set maxPoints, or trim yourself to keep memory flat.