Built with ApexCharts.js, ApexMaps, ApexGrid
A web analytics dashboard is read in two moves: pick a period, then ask where the traffic came from and what it read. The dashboard above does both with ApexCharts.js for the sessions chart, its range brush and the traffic-source donut, ApexMaps for visitors by country, and ApexGrid for the top pages. Drag the brush and every panel recomputes for that date range; click a country and they all narrow to it.
The sales analytics dashboard filters on a click, which happens once. A brush filters on a drag, which is a stream of events, and every panel downstream pays for each one. This page is about wiring a brush into a dashboard: the event to listen to, which is not the obvious one, how often it fires, and what a series update does to the brush's target.
How do you filter a dashboard by a date range in ApexCharts?
Make a small brush chart that targets the main chart, and listen to its
brushScrolled event:
const detail = new ApexCharts(detailEl, {
chart: { id: 'sessions-detail', type: 'area' },
series, xaxis: { type: 'datetime' },
})
const brush = new ApexCharts(brushEl, {
chart: {
type: 'area',
brush: { target: 'sessions-detail', enabled: true, autoScaleYaxis: true },
selection: { enabled: true, xaxis: { min: from, max: to } },
events: {
brushScrolled: (chart, { xaxis }) => onBrush(xaxis.min, xaxis.max),
},
},
series, xaxis: { type: 'datetime' },
})
await detail.render() // the brush looks its target up by id when it renders
await brush.render()
Not events.selection. On the brush chart, the brush itself is implemented as
the selection handler, and ApexCharts only installs it when you have not
supplied one (setupBrushHandler in the core, checked at 7.8.0). Define
events.selection there and brushing is switched off in silence: the target
ignores both the initial window and every drag, and the page throws no error.
We hit exactly that while building this dashboard. brushScrolled fires from
the same place and leaves the brush working.
How often does the brush event fire?
That depends on which part of the brush the reader grabs, and the two gestures do not emit at the same rate. Measured on ApexCharts 7.8.0 with real pointer input:
Dragging the body of the selection rectangle fires one brushScrolled per
pointer move, every one of them before pointerup. Twenty moves produced
twenty events, forty moves produced forty, and dragging slowly changed nothing:
twenty moves spaced 60ms apart still produced twenty.
Dragging a handle, the small circle at either edge, puts the same event behind
a trailing debounce of about 30ms. A fast resize collapses to a single event
that arrives after pointerup: twenty moves spaced 10ms apart produced nothing
during the drag and one event 36.5 to 40.1ms after release across three runs,
and forty moves at that spacing produced one event 35.7ms after release. A slow
resize emits per move and then once more: twenty moves spaced 60ms apart
produced twenty events during the drag and a twenty-first 31.8 to 32.4ms after
release. Every handle resize leaves one event behind after the pointer is up,
fast or slow. The cut-over sits near the debounce interval: at 34.2ms between
moves, twenty moves produced thirteen events; at 24.3ms, four.
Either way the bounds arrive as fractional epoch milliseconds rather than whole days, and recomputing a world map, a donut and a grid once per pointer move is work nobody sees, so the handler does two things before it publishes:
function onBrush(min, max) {
// 1. Snap to whole days. Most pointer moves do not cross a day boundary.
pending = {
from: Math.max(0, Math.round((min - DAY0) / DAY)),
to: Math.min(DAYS - 1, Math.round((max - DAY0) / DAY)),
}
// 2. Coalesce to one publish per frame, and only if the range moved.
if (!frame) {
frame = requestAnimationFrame(() => {
frame = 0
if (pending.from !== filter.from || pending.to !== filter.to) {
filter.from = pending.from
filter.to = pending.to
publish('brush')
}
})
}
}
The line under the brush prints both counts after each drag, so the saving is on screen rather than claimed. A body drag of 40 pointer moves fires 40 brush events; how many of those survive as recomputes depends on how fast the reader drags and how many pixels a day is, which is why the line counts rather than predicts. It waits out the handle debounce before it prints, or a fast resize would report a gesture whose last event had not arrived yet.
What happens to the brush when the data changes?
Clicking a country replaces the series in both charts, and that exposes a
second trap. Measured: updateSeries() on the brush's target re-fits the
target to its full 90 days, while the brush window stays where it was, so the
two charts disagree about what is selected. Re-asserting the brush's own
selection re-drives the target:
subscribe(({ daily }, origin) => {
if (origin !== 'map' && origin !== 'reset') return // a brush move never redraws the brush
detail.updateSeries(series0(daily), false)
brush.updateSeries(series0(daily), false)
brush.updateOptions(
{ chart: { selection: { xaxis: { min: dayAt(filter.from), max: dayAt(filter.to) } } } },
false, false,
)
})
The same call is how Reset moves the window back. Note the first line too: a brush move must never trigger an update of the brush's own series, or it fights the drag in progress.
How do the other panels stay in sync?
The same store as every cross-filtered dashboard on this site: one plain
object, one derive step, and subscribers that re-read it. The brush owns
from and to, the map owns country. Each panel aggregates over everything
except the filter it is the source of: the map ignores country, or clicking
India would blank every other country and remove the control the reader just
used, and the brush chart ignores the date range for the same reason.
map.on('selectionChange', ({ ids }) => {
if (applying) return // setSelection() re-emits this event
filter.country = ids.length === 1 ? ids[0] : null
publish('map')
})
The grid's pages carry a rowId and a sort on views, so re-sorting by bounce
rate survives every brush move: apex-grid re-applies the reader's sort to each
new data array.
What breaks first
The brush event, as above. If a brush suddenly stops moving its target
after you add an event handler, check for events.selection on the brush
chart.
Render order. The brush finds its target with ApexCharts.getChartByID()
when it renders, and logs a warning and skips the target if it is not there
yet. Render the target first.
A table that never paints in a screenshot. ApexGrid renders rows lazily, only once the grid is scrolled into view. In a headless full-page capture the grid sits below the viewport and comes out empty, which looks like a broken grid and is not. Capture with a viewport tall enough to hold the page.
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.
The click-to-filter version: sales analyticsSee the pieces running
Reference documentation
Frequently Asked Questions
How do I filter a dashboard by a date range in ApexCharts?
Render the main chart with a chart.id, then a small brush chart with chart.brush.target set to that id and chart.selection.enabled. Listen to the brush chart's brushScrolled event, which reports the selected xaxis min and max, and feed that range into whatever filters the rest of the page.
Why did my brush chart stop moving its target?
Check for an events.selection handler on the brush chart. The brush is implemented as that handler and ApexCharts only installs it when you have not defined one (setupBrushHandler in the core, checked at 7.8.0), so defining it switches brushing off without an error. Use events.brushScrolled instead.
How often does brushScrolled fire during a drag?
It depends which part of the brush you grab. Measured on ApexCharts 7.8.0: dragging the selection rect fires once per pointer move, 40 events for a 40-move drag, all before pointerup. Dragging a resize handle goes through a 30ms trailing debounce, so a fast resize collapses to one event that lands about 35ms after pointerup, and every handle gesture leaves one trailing event at any speed. Bounds arrive in fractional milliseconds, so snap to your granularity, publish only when it changed, coalesce per animation frame, and do not read your counters on pointerup.
Why does the brush target jump back to the full range after updateSeries()?
Replacing the target's series re-fits it to all of its data while the brush window stays where it was. Re-assert the brush's selection afterwards with updateOptions({ chart: { selection: { xaxis: { min, max } } } }) on the brush chart, which re-drives the target.
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 analytics on your own traffic
ApexCharts, ApexGrid and ApexMaps are on every plan. Start with the installation guides.