Crossfilter (Categorical) in JavaScript

Using ApexCharts with JavaScript

This Crossfilter (Categorical) example uses ApexCharts.js directly in JavaScript, with no wrapper component.

Install it with npm install apexcharts, then mount the chart with new ApexCharts(element, options).render().

JavaScript installation guide
// Deterministic trade records (one row per trade). No randomness, but the
// distributions are intentionally uneven so the crossfilter is visible:
//   - quarters have different totals (the donut is 18 / 12 / 16 / 14, not 4x15)
//   - each quarter peaks on a different weekday
//   - outcome leans Gain early in the week and Loss later
// So clicking a quarter OR an outcome noticeably reshapes the day bar below.
function tradesData() {
  var days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']

  // quarter -> trades per weekday (Mon..Fri); column totals differ per quarter.
  var plan = {
    Q1: [8, 4, 3, 2, 1], // early-week heavy
    Q2: [1, 2, 5, 3, 1], // mid-week heavy
    Q3: [2, 2, 3, 4, 5], // late-week heavy
    Q4: [3, 6, 2, 2, 1], // Tuesday spike
  }
  // Share of Gains by weekday index (Mon..Fri): high early, low late.
  var gainByDay = [0.85, 0.7, 0.5, 0.3, 0.15]

  var out = []
  Object.keys(plan).forEach(function (q) {
    plan[q].forEach(function (count, di) {
      var gains = Math.round(count * gainByDay[di])
      for (var k = 0; k < count; k++) {
        out.push({
          q: q,
          day: days[di],
          gl: k < gains ? 'Gain' : 'Loss',
        })
      }
    })
  })
  return out
}

// Register the shared record set BEFORE the charts are constructed, so each
// chart's initial paint is already the aggregation (no empty flash).
ApexCharts.crossfilter({ id: 'trades', records: tradesData() })

// Readout formatter, shared by the vanilla-js, React and Vue builds.
function fmtFilters(state) {
  var keys = Object.keys(state.filters)
  if (!keys.length) return 'No filter (all ' + state.total + ' trades)'
  var parts = keys.map(function (id) {
    return id + ': ' + state.filters[id].join(', ')
  })
  return (
    parts.join('  |  ') +
    '   ->   ' +
    state.filteredCount +
    ' / ' +
    state.total +
    ' trades'
  )
}

var options = {
  series: [],
  chart: {
    id: 'byQuarter',
    type: 'donut',
    height: 300,
    fontFamily: 'Helvetica, Arial, sans-serif',
    animations: { speed: 500 },
    link: {
      id: 'trades',
      dimension: function (r) {
        return r.q
      },
      reduce: 'count',
      dimOpacity: 0.18,
    },
  },
  title: { text: 'By quarter', align: 'left' },
  legend: { position: 'bottom' },
  plotOptions: { pie: { expandOnClick: false } },
  dataLabels: {
    enabled: true,
    formatter: function (val, opts) {
      return opts.w.config.series[opts.seriesIndex]
    },
    style: { colors: ['#334155'], fontWeight: 600 },
    dropShadow: { enabled: false },
  },
  colors: ['#2563EB', '#38bdf8', '#4ade80', '#fbbf24'],
  stroke: { width: 2, colors: ['#fff'] },
}

var chart = new ApexCharts(document.querySelector('#chart'), options)
chart.render()

var options1 = {
  series: [],
  chart: {
    id: 'byOutcome',
    type: 'donut',
    height: 300,
    fontFamily: 'Helvetica, Arial, sans-serif',
    animations: { speed: 500 },
    link: {
      id: 'trades',
      dimension: function (r) {
        return r.gl
      },
      reduce: 'count',
      order: 'asc', // Gain before Loss, so the colors below map semantically
      dimOpacity: 0.18,
    },
  },
  title: { text: 'By outcome', align: 'left' },
  legend: { position: 'bottom' },
  plotOptions: { pie: { expandOnClick: false } },
  dataLabels: {
    enabled: true,
    formatter: function (val, opts) {
      return opts.w.config.series[opts.seriesIndex]
    },
    style: { colors: ['#334155'], fontWeight: 600 },
    dropShadow: { enabled: false },
  },
  colors: ['#4ade80', '#f87171'],
  stroke: { width: 2, colors: ['#fff'] },
}

var chart1 = new ApexCharts(document.querySelector('#chart2'), options1)
chart1.render()

var options2 = {
  series: [],
  chart: {
    id: 'byDay',
    type: 'bar',
    height: 280,
    fontFamily: 'Helvetica, Arial, sans-serif',
    animations: { speed: 500 },
    link: {
      id: 'trades',
      dimension: function (r) {
        return r.day
      },
      reduce: 'count',
      seriesName: 'Trades',
      // `order` also takes a comparator: keep the weekdays in calendar order
      // instead of the order they first appear in the records.
      order: function (a, b) {
        var days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
        return days.indexOf(a) - days.indexOf(b)
      },
      dimOpacity: 0.18,
    },
  },
  title: { text: 'By day of week (click a bar too)', align: 'left' },
  plotOptions: {
    bar: { columnWidth: '55%', borderRadius: 3, distributed: true },
  },
  legend: { show: false },
  dataLabels: { enabled: false },
  colors: ['#2563EB', '#38bdf8', '#4ade80', '#fbbf24', '#f472b6'],
}

var chart2 = new ApexCharts(document.querySelector('#chart3'), options2)
chart2.render()

// charts[0]=byQuarter, charts[1]=byOutcome, charts[2]=byDay.
// The generated file exposes them as chart, chart1, chart2. The wiring talks to
// the crossfilter engine (not the chart instances), and fmtFilters lives in the
// shared head script.
var cf = ApexCharts.getCrossfilter('trades')
var readout = document.getElementById('cf-readout')

if (cf) {
  cf.on('change', function (state) {
    readout.textContent = fmtFilters(state)
  })
}

document.getElementById('cf-reset').addEventListener('click', function () {
  if (cf) cf.reset()
})