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?
Once per pointer move. Measured on ApexCharts 7.8.0, a drag of 40 pointer moves
produced 40 brushScrolled events, and the bounds arrive as fractional epoch
milliseconds rather than whole days. Recomputing a world map, a donut and a
grid 40 times a second 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. In our test a 60-move drag ran 60 brush events and 30 recomputes; how many you save depends on how fast the reader drags and how many pixels a day is.
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?
Once per pointer move: measured on ApexCharts 7.8.0, 40 events for a 40-move drag, with bounds in fractional milliseconds. Snap the range to your data's granularity, publish only when it changed, and coalesce to one update per animation frame.
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.