Showcase

Realtime Monitoring Dashboard

Three Apex products on a feed that updates every second: a streaming throughput chart, a map of regions and a table of hosts. Pick a region and every panel narrows to it while the data keeps moving; sort or select in the table and it holds.

Realtime monitoring dashboard: streaming chart, region map and host grid updating every secondOpen in new tab

Built with ApexCharts.js, ApexGrid, ApexMaps

A monitoring dashboard is three questions at once: how much traffic is there, where is it, and which machine is in trouble. The dashboard above answers them with ApexCharts.js for the streaming throughput and latency charts, ApexMaps for the regions, and ApexGrid for the hosts, and it updates every second. Click a region on the map or a bar in the latency chart and every panel narrows to it while the data keeps arriving.

The cross-filter itself is the same contract as the sales analytics dashboard. What is new here is that the data moves under the reader's hands. A sort, a selected row and a selected region all have to survive a write that lands every second, and each library treats that write differently. This page is about those differences, measured on the versions the dashboard loads.

How do you stream data into an ApexCharts line chart?

Append points; do not replace a fixed window. Each point keeps its x value as new ones arrive, and xaxis.range slides the visible window over them, so the line translates left like a strip chart.

const throughput = new ApexCharts(el, {
  chart: {
    type: 'area',
    animations: { dynamicAnimation: { speed: 1000 } }, // equal to the tick
  },
  xaxis: { type: 'datetime', range: 119 * 1000 },     // two minutes visible
  series: [{ name: 'Requests / s', data: history }],
})

// every tick
throughput.appendData([{ data: [[now, value]] }])

The tempting alternative, keeping the last 120 points and calling updateSeries() with them every second, does not scroll. Index 0 is a different point on every tick but it is drawn at the same pixel, so the line morphs in place instead of moving.

Two details decide whether the scroll looks clean:

  • xaxis.range does not drop data. It only moves the axis. appendData() only ever pushes, so the chart's own series grows for as long as the page is open. Trim it occasionally, without animation, once it is a few windows long: updateSeries([{ data: history.slice(-120) }], false). The dropped points are off-screen, so nothing visibly changes.
  • Use the stream's timestamps, not the wall clock at render time. Timers drift and are throttled in background tabs, which makes the x steps uneven and the scroll judder. The dashboard advances its own clock by exactly one tick per update.

The real-time charts guide has the full option reference.

Should a live grid get new row objects on every update?

No, and this is the finding that surprised us. Sort and scroll position both survive grid.data = newArray on apex-grid 3.5.0: the grid re-applies the reader's sort to every new array. A selected row is where the two ways of writing diverge.

Each tick doesHighlight on screengrid.selectedRows
rows.map((r) => ({ ...r, cpu })), new objectsdisappearsstill one row, frozen at the values it had when clicked
same objects, mutated, in a new arraystays on the row as it moves through the sortthe live object, current values

Setting rowId does not change the first row of that table. It governs what getState() captures, not live selection.

So the immutable update that most front-end code reaches for by habit is the wrong one here. Keep each row object for the life of the page, write the new values into it, and hand the grid a new array so it repaints:

// HOSTS holds one object per host, created once.
function tick() {
  for (const h of HOSTS) {
    h.cpu = nextCpu(h)
    h.p95 = nextLatency(h)
    // A NEW array on the same row object, so the sparkline cell sees a
    // changed value and redraws.
    h.cpuTrend = [...h.cpuTrend.slice(-19), h.cpu]
  }
}

subscribe(({ rows }) => {
  grid.data = rows.slice()
})

The one field that does need a new value is the array behind a type: 'sparkline' column. Pushing onto the existing array leaves the cell holding the same reference, so it has no reason to redraw.

The readout in the toolbar ("Selected us-east-web-04: 96% CPU") reads grid.selectedRows on every tick. With stable rows it shows live numbers. With fresh objects it would show the moment of the click, forever, and nothing on screen would say so.

Does a map selection survive a live update?

Yes. On ApexMaps 0.4.0, map.updateSeries() emits only updated and leaves the selected bubble selected, so the per-second write needs no code to restore it.

map.setSelection() is different: it emits selectionChange, the same event a click emits. Pushing the scope back into the map from the latency chart would re-enter the store without a guard, which is why the dashboard keeps the applying flag from the static cross-filter:

map.on('selectionChange', ({ ids }) => {
  if (applying) return
  setRegion(ids.length === 1 ? ids[0] : null, 'map')
})

subscribe(({ byRegion }, origin) => {
  map.updateSeries([bubbleSeries(byRegion)])     // every tick, no echo
  if (origin === 'map' || origin === 'tick') return
  if (view.region) map.setSelection([view.region])  // echoes: guarded
  else map.clearSelection()
})

Every subscriber receives origin, because a live dashboard has two reasons to publish: the data moved ('tick'), or the reader changed the scope. The throughput chart appends on a tick and swaps in the other region's history, unanimated, on a scope change. Doing the same work for both is how a scope change ends up animating a meaningless morph between two unrelated lines.

Why pin the scale domains on a live map?

Because a scale derived from the data is re-derived on every update, and then the map cannot show a change that happens everywhere at once. Measured on ApexMaps 0.4.0 with three bubbles and no size.domain: halving every region's traffic left the radii exactly where they were, 30, 23.9 and 19.9 pixels before and after. The busiest region always gets the largest bubble, so a fleet-wide drop is invisible. With domain: [0, 1000] the same update shrank the largest bubble from 28.8 to 22.1 pixels. Pin the size domain, and the colour scale too, and a bubble means the same requests per second at 09:00 and at 09:02:

size: { scale: 'sqrt', range: [5, 24], domain: [0, 3000] },
colorField: 'errors',
colorScale: { type: 'threshold', breaks: [1, 2, 4], palette: ['#10b981', '#f59e0b', '#f97316', '#ef4444'] },

The same reasoning puts a fixed max on the latency chart's axis and a dashed SLO line at 250 ms, so a bar's length is comparable across seconds.

What breaks first

The loop running in a background tab. Browsers throttle timers in hidden tabs, so a loop left running produces either a slow trickle or a burst of catch-up work, and nobody is looking at it. The dashboard pauses on visibilitychange and resumes when the tab is visible again. A real feed would also unsubscribe on the server.

Selection drawn by the chart's own active state. A bar chart that is updated every second keeps resetting its selected bar. The latency chart draws the selected region with a colour function instead, which runs on every update:

colors: [({ value, dataPointIndex }) =>
  view.region && REGIONS[dataPointIndex].id !== view.region
    ? '#cbd5e1'
    : value > SLO_MS ? '#ef4444' : '#008ffb'],

Bar labels that flash once a second. On ApexCharts 7.8.0, updateSeries() on a bar chart rebuilds the data-label group hidden and only reveals it when the bar animation finishes, so at one update a second the labels are missing about 40% of the time. updateOptions({ series }) reveals them immediately, and dataLabels.countUp then counts each number to its new value as the bar moves:

dataLabels: { enabled: true, countUp: { enabled: true }, formatter: (v) => `${Math.round(v)} ms` },
// every tick
latency.updateOptions({ series: [{ name: 'p95', data: byRegion.map((r) => r.p95) }] })

Re-sorting the bars by value. dataPointSelection reports a category index. Sort the regions by latency and every index silently points at a different region.

Which plan covers this?

ApexCharts.js, ApexGrid and ApexMaps are included on every plan, including Community, which is free for organizations under $2M in annual revenue, budget or funding. Nothing in this dashboard is a gated feature, and the file carries no license key. Redistributing the libraries inside software you sell or host is covered by the OEM plan.

Feed a dashboard like this from a WebSocket

See the pieces running

Reference documentation

Frequently Asked Questions

How do I make a realtime chart in ApexCharts?

Append each new point with appendData() and set xaxis.range to the width of the window you want visible, so the line scrolls. Set chart.animations.dynamicAnimation.speed equal to your update interval. The range only moves the axis and never drops data, so trim the series now and then with an unanimated updateSeries() call, or it grows for as long as the page is open.

Why does a selected grid row show old values after a live update?

Because each update built new row objects. Measured on apex-grid 3.5.0: when grid.data is replaced with new objects, the selection highlight disappears but grid.selectedRows still returns the old object with the values from the moment it was clicked, and setting rowId does not change that. Keep one object per row, write the new values into it, and assign a new array so the grid repaints. The highlight then stays and the selection reads live values.

Do sort and scroll position survive replacing a grid's data every second?

Yes. apex-grid re-applies the reader's sortExpressions to every new data array, and the scroll position is kept. Selection is the one piece of state that depends on how you write the update.

Does an ApexMaps selection survive updateSeries()?

Yes. On ApexMaps 0.4.0, updateSeries() emits only the updated event and leaves the selected bubble or region selected. setSelection() is the call to guard: it emits selectionChange, the same event a click emits.

Which plan covers building this?

ApexCharts.js, ApexGrid and ApexMaps are all included on every plan, including Community, which is free for organizations under $2M in annual revenue, budget or funding. Nothing in the dashboard is a gated feature. Redistributing them inside software you sell or host requires the OEM plan.

Related

Build a live dashboard on your own feed

ApexCharts, ApexGrid and ApexMaps are on every plan. Start with the installation guides.

Get started