Real-Time Charts

ApexCharts streams live data by appending points to a series and letting a fixed xaxis.range slide across them. Because appended points keep their position, each new point extends the axis and the window scrolls left over the fixed points, producing a continuous leftward scroll rather than a redraw. The one thing you manage yourself is memory: range controls only the visible window, not the size of the data array, so you trim old off-screen points periodically.


Bounded streaming (ApexCharts 6.0)

In ApexCharts 6.0 you can hand memory management to the library. Set chart.streaming.enabled: true and appendData() trims each series automatically, to maxPoints when set, otherwise to the visible xaxis.range window plus a small runway, so an always-on feed never grows the data array without limit:

const options = {
  chart: { streaming: { enabled: true, maxPoints: 100000 } },
}

The constant-velocity scroll animation needs no opt-in: any update that continues the previous window (appendData, or a shifted fixed-length updateSeries) slides smoothly. See chart.streaming. The rest of this guide shows the mechanics of the append-and-scroll pattern, which is still useful when you want to trim manually or support older versions.

Scrolling with appendData

Add each new value with appendData. Set xaxis.range to the visible window, use a datetime axis, and match dynamicAnimation.speed to how often points arrive.

import ApexCharts from 'apexcharts'

const options = {
  chart: {
    type: 'line',
    animations: {
      enabled: true,
      dynamicAnimation: {
        speed: 1000 // match the update interval in ms
      }
    },
    toolbar: { show: false },
    zoom: { enabled: false } // prevent accidental zoom while watching live data
  },
  series: [{ name: 'Sensor', data: [] }],
  xaxis: {
    type: 'datetime',
    range: 30_000 // show a 30-second rolling window
  },
  yaxis: {
    min: 0,
    max: 100
  }
}

const chart = new ApexCharts(document.querySelector('#chart'), options)
await chart.render()

const intervalId = setInterval(() => {
  chart.appendData([{ data: [{ x: Date.now(), y: Math.random() * 100 }] }])
}, 1000)

// On page unload or component unmount:
// clearInterval(intervalId)
// chart.destroy()

Why appending scrolls: appendData leaves the existing points untouched and adds the new one at the leading edge. As the axis max advances to the new point, every fixed point's pixel position moves left by one step, so the line translates. Keep the gap between points even (a steady cadence, or server timestamps at a fixed interval) so each scroll step is the same width.

Why dynamicAnimation.speed should match the interval: ApexCharts animates from the previous frame to the new one over speed milliseconds. If speed is shorter than the interval, the animation finishes early and the chart sits still until the next tick, which creates a stutter. If speed is longer than the interval, animations pile up and the chart lags behind. Setting them equal means each animation completes exactly as the next point arrives.

Why zoom: { enabled: false }: when a user drags to zoom on a live chart, the next update resets the zoom range. Disabling zoom prevents that jarring experience.


Bounding memory

xaxis.range sets the visible window (it computes the axis min and max); it does not remove points from the series. appendData only pushes, so the underlying array grows for as long as the stream runs. To keep memory flat, trim the old off-screen points yourself, periodically, with a non-animated updateSeries:

// Track your own data so you can trim it. Not every tick — only when the
// buffer has grown well past the visible window.
const VISIBLE = 30
if (data.length > 3 * VISIBLE) {
  data = data.slice(-2 * VISIBLE)
  chart.updateSeries([{ data }], false) // second arg false => no animation
}

The trim is invisible because the points you drop are off-screen, so the visible line is unchanged, and false skips the animation so there is no flicker.

Do not trim on every tick. Dropping the oldest point and replacing a shifting window each tick does not scroll: under the rescaled axis, each point keeps its pixel position and only its value changes, so the line appears to warp in place ("crawl to the previous shape") instead of translating. Append points and trim only occasionally.


WebSocket streaming

Wire a WebSocket to the same appendData. The message handler replaces the interval.

import ApexCharts from 'apexcharts'

const chart = new ApexCharts(document.querySelector('#chart'), {
  chart: {
    type: 'line',
    animations: { enabled: true, dynamicAnimation: { speed: 500 } },
    toolbar: { show: false },
    zoom: { enabled: false }
  },
  series: [{ name: 'Price', data: [] }],
  xaxis: { type: 'datetime', range: 60_000 },
  yaxis: { min: 0 }
})

await chart.render()

const socket = new WebSocket('wss://your-api.example.com/stream')

socket.addEventListener('message', (event) => {
  const { value } = JSON.parse(event.data)
  // Fire-and-forget: do not await here
  chart.appendData([{ data: [{ x: Date.now(), y: value }] }])
})

// On page unload or component unmount:
// socket.close()
// chart.destroy()

Do not await appendData inside a WebSocket handler. Messages can arrive faster than render cycles. Awaiting would cause the handler to queue up unprocessed messages, and each subsequent message would wait for the previous render to complete before being processed. For very high-rate feeds, do not touch the chart on every message: buffer incoming values and append the newest batch on a timer or requestAnimationFrame, so the render rate is decoupled from the message rate. If the server emits at a fixed cadence, use the timestamp from the payload as x so the scroll steps stay even.


Multi-series real-time

Pass one entry per series in the appendData array, matched positionally to the existing series.

chart.appendData([
  { data: [{ x: Date.now(), y: sensor1.read() }] }, // series[0]
  { data: [{ x: Date.now(), y: sensor2.read() }] }  // series[1]
])

All series share the same xaxis.range window. If you need different Y scales, configure multiple Y axes with the yaxis array and assign each series to an axis by index.


Common mistakes

Assuming xaxis.range bounds memory

It does not. range is a view setting; the series array keeps every point you append until you trim it. Periodically slice the array (non-animated updateSeries) so a stream that runs for hours does not grow unbounded.

Trimming on every tick (the in-place warp)

Replacing a drop-oldest window each tick makes the update a shape morph, not a translate: under the rescaled axis every point sits at the same pixel, so the line warps in place instead of scrolling. Append points; trim only occasionally.

Uneven point spacing

If the gap between points varies (an imprecise timer, a throttled background tab), the scroll speed jitters. Use a steady cadence or fixed-interval server timestamps.

Not matching dynamicAnimation.speed to the update interval

If speed does not match the interval, the chart either idles between ticks (speed too low) or falls behind (speed too high). Set them to the same value.

Leaving zoom enabled on a live chart

zoom: { enabled: false } is not just UX polish. A zoom state changes the internal axis range, and the next update resets it, which causes a visible snap. Disable zoom on any chart receiving continuous updates.

Not cleaning up on unmount

If the interval or socket continues running after the chart is destroyed, each subsequent update targets a destroyed instance. This leaks memory and may throw. Always pair setup with cleanup:

const intervalId = setInterval(tick, 1000)
window.addEventListener('beforeunload', () => {
  clearInterval(intervalId)
  chart.destroy()
})

In React, return the cleanup from useEffect. In Vue, use onUnmounted. In Angular, use ngOnDestroy.


React example

Use useRef to hold the chart instance so the interval callback always has the current chart without triggering re-renders. Set up the interval in useEffect and return the cleanup function.

import { useRef, useEffect } from 'react'
import ApexCharts from 'apexcharts'

function RealtimeChart() {
  const elRef = useRef(null)
  const chartRef = useRef(null)

  const options = {
    chart: {
      type: 'line',
      animations: { enabled: true, dynamicAnimation: { speed: 1000 } },
      toolbar: { show: false },
      zoom: { enabled: false }
    },
    series: [{ name: 'Sensor', data: [] }],
    xaxis: { type: 'datetime', range: 30_000 },
    yaxis: { min: 0, max: 100 }
  }

  useEffect(() => {
    const chart = new ApexCharts(elRef.current, options)
    chartRef.current = chart
    chart.render()

    const id = setInterval(() => {
      chartRef.current?.appendData([{ data: [{ x: Date.now(), y: Math.random() * 100 }] }])
    }, 1000)

    return () => {
      clearInterval(id)
      chart.destroy()
    }
  }, [])

  return <div ref={elRef} />
}

Why useRef instead of state: the interval callback needs the live chart on every tick without re-rendering the component. useRef holds a mutable reference that persists across renders without causing them, so the chart instance is created once and never torn down mid-stream.

Why the empty dependency array on useEffect: the interval should start once when the component mounts and stop when it unmounts. Listing dependencies would restart the interval when those values change, which is not the behavior you want for a continuous stream.


For the full appendData and updateSeries API reference, including how to update multiple series or skip a series, see Update Chart Data Dynamically.