Linked Views is the coordinated-dashboard feature in ApexCharts 6.0, and its engine is crossfilter. The idea is old and powerful: put several charts over one dataset, and let a selection in any chart filter all the others. Click "Loss" in an outcome donut and every other chart redraws to show only losing trades. Brush a range on a histogram and the donut's mix shifts. ApexCharts now does this natively: you register one record set, give each chart a dimension, and the coordination is automatic.

This post builds a working crossfilter dashboard, explains the two linking modes, and covers the one rule that makes the interaction feel right (each chart filters the others, never itself).

Key takeaways

  • Two modes. Highlight mode dims matching marks across charts that share a chart.group. Filter mode (crossfilter) re-aggregates charts over a shared record set. The presence of a dimension function selects filter mode.
  • One record set, many dimensions. ApexCharts.crossfilter({ id, records }) registers the data; each chart's chart.link.dimension maps a row to a bucket key.
  • Each chart filters the others, not itself. Clicking a bar filters every other chart but leaves that chart showing all buckets, so you can keep exploring.
  • Category vs range. Donuts and bars filter by clicking a bucket; histograms filter by brushing a range (add bins, enable selection).
  • A bound data table (cf.dataTable) lists the surviving rows and re-renders on every change.

See it live

Click a slice in the Outcome donut, click a bar in the Day chart, or drag across the Fluctuation histogram to brush a range. Every other view and the table below re-aggregate over the rows that pass all the other filters. Reset all filters clears everything.

Three views over one record set. Click a donut slice or a day bar to filter by that category; drag across the fluctuation histogram to brush a range. Every other chart, and the table, re-aggregate over the rows that pass all the other filters. Brush a range and watch the outcome mix shift.

Register the record set first

The record set is the source of truth. Register it once, by id, before you construct the linked charts, so each chart can attach its dimension as it renders.

import ApexCharts from 'apexcharts'
import 'apexcharts/features/link'

const trades = [
  { id: 1, quarter: 'Q1', day: 'Mon', outcome: 'Gain', pct: 1.4, volume: 180 },
  // ...one plain object per record
]

ApexCharts.crossfilter({ id: 'trades', records: trades })

crossfilter() is get-or-create: call it again with the same id to retrieve the coordinator, or ApexCharts.getCrossfilter('trades') to look it up.

Give each chart a dimension

A chart joins the crossfilter by declaring a chart.link with the coordinator id and a dimension function. The function maps a record to its bucket key. That is all a category chart needs:

const outcomeDonut = new ApexCharts(el, {
  series: [], // crossfilter fills this in
  chart: {
    type: 'donut',
    link: {
      id: 'trades',
      dimension: (row) => row.outcome, // buckets: 'Gain' | 'Loss'
      reduce: 'count',
      dimOpacity: 0.18,
    },
  },
})
outcomeDonut.render()

Note series: []. You do not compute the aggregation yourself; the crossfilter engine reduces the records into the chart's series. reduce defaults to 'count' and also accepts { sum: 'field' }, { avg: 'field' }, or a function over the bucket's rows.

Category buckets vs numeric ranges

The dimension's shape decides the interaction:

ChartDimensionFilter interactionExtra config
Donut / bar (categorical)(r) => r.categoryClick a bucketnone
Histogram (numeric range)(r) => r.value + binsBrush a rangechart.selection.enabled + the selection toolbar tool

A range chart adds bins and enables brushing:

const pctHistogram = new ApexCharts(el, {
  series: [],
  chart: {
    type: 'bar',
    selection: { enabled: true },
    toolbar: { autoSelected: 'selection', tools: { selection: true, zoom: false, pan: false, reset: false, download: false } },
    link: {
      id: 'trades',
      dimension: (row) => row.pct,
      bins: { count: 24 }, // range dimension: 24 buckets across the domain
      dimOpacity: 0.16,
    },
  },
})

autoSelected: 'selection' arms brushing so the reader can just drag, no tool click first.

The rule that makes it feel right

The reason a crossfilter dashboard feels natural is a deliberate asymmetry:

Each chart re-aggregates over the rows passing all the OTHER charts' filters, never its own.

So when you click "Mon" in the Day chart, the Outcome donut and the Fluctuation histogram narrow to Monday's trades, but the Day chart keeps showing every day. If a chart filtered itself, clicking one bar would collapse it to a single bar, and you could never pick a different day. Because it filters only the others, every chart stays a usable control after you have selected in it.

Bind a data table

The coordinator can render a live table of the surviving rows, re-rendered on every filter change:

const cf = ApexCharts.getCrossfilter('trades')

cf.dataTable(document.querySelector('#table'), {
  columns: [
    { field: 'id', label: '#' },
    { field: 'day', label: 'Day' },
    { field: 'outcome', label: 'Outcome' },
    { field: 'pct', label: 'Fluctuation %', format: (v) => `${v.toFixed(2)}%` },
  ],
  pageSize: 8,
})

// react to changes yourself, too
cf.on('change', (state) => {
  console.log(`${state.filteredCount} / ${state.total} rows`, state.filters)
})

// clear everything
cf.reset()

cf.state() returns the active filters plus filteredCount and total at any time, which is what the demo's readout shows.

Highlight mode: linking without a record set

If you do not need re-aggregation, the lighter highlight mode links charts that share a chart.group. Enable chart.link.enabled with no dimension, and brushing a range on one member dims out-of-range marks across every member in place, with no re-render. It is the right tool when the charts already show the same x domain and you just want a shared spotlight rather than a filtered subset.

When to use which

  • Filter mode (crossfilter): several charts are different views of one dataset and you want selection in one to subset the rest. This is the dashboard case.
  • Highlight mode: charts share an x axis and you want a linked spotlight without changing what each chart aggregates.
  • Neither: a single chart, or charts over unrelated datasets. Do not add crossfilter to charts that do not share data.

Summary

Linked Views brings native crossfilter to ApexCharts. Register one record set with ApexCharts.crossfilter({ id, records }), give each chart a chart.link with a dimension, and selection in any chart re-aggregates the others over the filtered rows, with each chart filtering the others but not itself. Category charts filter by click, range charts by brush, and cf.dataTable keeps a live table of the survivors. Import apexcharts/features/link, and see the Linked Views guide for the full API and the interactivity demos for more configurations. It is one of the investigation features in ApexCharts 6.0.

Frequently asked questions

What is crossfilter in ApexCharts?

Crossfilter is the filter engine behind Linked Views in ApexCharts 6.0. You register one shared record set with ApexCharts.crossfilter({ id, records }), and each chart declares a dimension (a function from a row to a bucket key). Clicking a category bucket or brushing a numeric range filters the records, and every other participating chart re-aggregates over the rows that pass all the other charts' filters. It is the classic crossfilter interaction: coordinated views over one dataset.

How do I link ApexCharts together?

There are two modes. Highlight mode links charts that share a chart.group: set chart.link.enabled and brush a range, and matching marks dim across all members with no re-render. Filter mode is the full crossfilter: register a record set with ApexCharts.crossfilter, and give each chart a chart.link with an id and a dimension function. The presence of dimension selects filter mode. Import the feature with import 'apexcharts/features/link'.

How does each chart know which rows to show?

The crossfilter coordinator computes, for each chart, the rows that pass every OTHER chart's filter, never its own. That is what makes the interaction feel right: clicking a bar in the Day chart filters the Outcome and Fluctuation charts but leaves the Day chart showing all days so you can pick another. A bound data table (cf.dataTable) shows the rows passing every active filter.

Can I brush a numeric range instead of clicking a category?

Yes. Give the chart a range dimension by adding bins to chart.link (for example bins: { count: 24 }) and enable selection with chart.selection.enabled and the selection toolbar tool. Dragging across the chart brushes a min-max range, which becomes that chart's filter. Category charts (donuts, bars) filter by clicking a bucket; range charts (histograms) filter by brushing.