Built with ApexCharts.js, ApexGrid, ApexMaps
A sales dashboard needs three things on one screen: a map to see where revenue is, a chart to see how it is moving, and a table to see the accounts behind it. The dashboard above is all three, from ApexCharts.js, ApexGrid and ApexMaps, and they are cross-filtered: click a state and the table and charts follow.
Each library's own configuration is already documented. What is not documented anywhere, in these docs or anyone else's, is the seam: how three independent components agree on what "currently selected" means. That is what this page is about.
How do three components stay in sync?
Not by talking to each other. Three libraries have three event vocabularies:
ApexMaps emits selectionChange with feature ids, ApexCharts emits
dataPointSelection with series and point indices, ApexGrid raises DOM events
off a custom element. Wiring each to the others means an adapter per pair, and
every component's state ends up depending on every other component's event
order.
Instead there is one plain object, one function that derives everything from it, and a list of subscribers. Each component publishes into the filter and re-reads what the derive step produced. It knows nothing about the others.
const filter = { state: null, industry: null }
const subscribers = []
let applying = false
function apply(origin) {
if (applying) return
applying = true
try {
const rows = ACCOUNTS.filter(matches)
for (const fn of subscribers) fn(rows, origin)
} finally {
applying = false
}
}
Adding a fourth component adds one publisher and one subscriber. It does not add three adapters.
Why is there an applying guard?
Because setting a selection in code makes a library emit the same event a user click emits. Without the guard, the first click ping-pongs:
user clicks CA
-> map emits selectionChange
-> apply() runs
-> map.setSelection(['CA']) // push state back to the map
-> map emits selectionChange // indistinguishable from a click
-> apply() runs again ...
This is not an ApexMaps quirk. Any cross-filtered dashboard hits it, because "the user did this" and "code did this" are the same event. Two lines fix it, and they are the two lines most cross-filter examples leave out.
The map's own subscriber also checks where the change came from:
subscribe((rows, origin) => {
if (origin === 'map') return // do not re-select on our own event
if (filter.state) map.setSelection([filter.state])
else map.clearSelection()
})
Wiring each component into the filter
The map publishes a state. multiple: false matters: the default toggles a
feature in and out, which accumulates states. A single-region filter wants a
click to replace the selection.
const map = new ApexMaps(el, {
geo: { map: 'us' },
interaction: { selection: { enabled: true, multiple: false } },
series: [{ name: 'Revenue', joinBy: ['abbr', 'key'], data, scale: { palette: 'blues' } }],
})
map.on('selectionChange', ({ ids }) => {
filter.state = ids.length === 1 ? ids[0] : null
apply('map')
})
Map click-selection needs no license key. Only linked maps, two or more maps
sharing a link.group, are a Premium feature.
The chart publishes an industry. dataPointSelection gives you a category
index, not a label, so the axis order is load-bearing: re-sorting the bars by
value would silently repoint every index.
chart: {
events: {
dataPointSelection(event, ctx, config) {
const name = INDUSTRIES[config.dataPointIndex]
filter.industry = filter.industry === name ? null : name
apply('industry')
},
},
}
The grid just re-reads rows. ApexGrid has its own filtering, and this dashboard does not use it, on purpose: one definition of "selected" for the whole page beats two that have to agree.
subscribe((rows) => {
grid.data = rows.slice().sort((a, b) => b.revenue - a.revenue).slice(0, 200)
})
What breaks first
Three things, in the order you will hit them.
The bar chart filtering itself away. If the industry bars aggregate over the same filtered rows as everything else, clicking a bar collapses the chart to that one bar and removes the control the reader just used. The fix is that each filter source aggregates over rows filtered by everything except itself:
const scoped = ACCOUNTS.filter((r) => !filter.state || r.state === filter.state)
industryChart.updateSeries([{ name: 'Revenue', data: byIndustry(scoped).map((d) => d.revenue) }])
Charts that will not shrink. An ApexCharts SVG inside a CSS grid item needs
min-width: 0 on the item. Without it the chart refuses to shrink below its
rendered width and pushes the page sideways at narrow viewports.
Render order. The map fetches its geometry, so render() is async while the
charts are not. Await it before the first apply() or the map misses the
initial state.
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. Nothing in this dashboard is a gated feature. Redistributing the libraries inside software you sell or host is covered by the OEM/Embedded plan.
Components or a BI platform: how to chooseSee the pieces running
Reference documentation
Frequently Asked Questions
Can one library do charts, tables and maps in the same dashboard?
Yes. This dashboard uses three products from the same family: ApexCharts.js for the revenue and trend charts, ApexGrid for the account table, and ApexMaps for the region map. They share a licence and a configuration style, and they communicate through ordinary DOM events rather than a proprietary bus.
How do the three components stay in sync?
One plain JavaScript object holds the current filter, and each component publishes its selection into it and then re-reads it. There is no framework and no state library involved: the map emits a region, the store records it, and the grid and charts each recompute from the same filtered rows.
What does cross-filtering three components cost in bundle size?
The dashboard loads ApexCharts.js, ApexGrid and ApexMaps from the CDN as three separate scripts. Each is independently tree-shakeable, so a real application ships only the chart types and grid features it uses rather than all three libraries in full.
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. Redistributing them inside software you sell or host requires the OEM/Embedded plan.
Related
Build this on your own data
ApexCharts, ApexGrid and ApexMaps are on every plan. Start with the installation guides.