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.rangedoes 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 does | Highlight on screen | grid.selectedRows |
|---|---|---|
rows.map((r) => ({ ...r, cpu })), new objects | disappears | still one row, frozen at the values it had when clicked |
| same objects, mutated, in a new array | stays on the row as it moves through the sort | the 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 WebSocketSee 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.