Crossfilter Dashboard in Vue

Using ApexCharts with Vue

This Crossfilter Dashboard example wires ApexCharts into Vue through the vue3-apexcharts component.

Install it with npm install vue3-apexcharts apexcharts, then mount the chart with <apexchart type="..." :options="options" :series="series" />.

Vue charts guide
<template>
  <div>
    <div class="cfd">
      <div class="cfd-bar">
        <button id="cfd-reset">Reset all filters</button>
        <span class="readout" id="cfd-readout"></span>
      </div>

      <div class="cfd-grid">
        <div class="cfd-card">
          <div id="chart">
            <apexchart
              type="donut"
              height="260"
              :options="chartOptions"
              :series="series"
            ></apexchart>
          </div>
        </div>
        <div class="cfd-card">
          <div id="chart2">
            <apexchart
              type="donut"
              height="260"
              :options="chartOptions1"
              :series="series1"
            ></apexchart>
          </div>
        </div>
        <div class="cfd-card">
          <div id="chart3">
            <apexchart
              type="bar"
              height="260"
              :options="chartOptions2"
              :series="series2"
            ></apexchart>
          </div>
        </div>
        <div class="cfd-card brush">
          <div id="chart4">
            <apexchart
              type="bar"
              height="240"
              :options="chartOptions3"
              :series="series3"
            ></apexchart>
          </div>
        </div>
        <div class="cfd-card brush">
          <div id="chart5">
            <apexchart
              type="bar"
              height="240"
              :options="chartOptions4"
              :series="series4"
            ></apexchart>
          </div>
        </div>
      </div>

      <div class="cfd-tablewrap" id="cfd-table"></div>

      <div class="cfd-note">
        Five views over one record set registered with
        <code>ApexCharts.crossfilter(&#123; id, records &#125;)</code>. The
        donuts and the day bar filter by <b>clicking a bucket</b>; the two
        histograms filter by <b>brushing a range</b> (they set
        <code>chart.selection.enabled</code>). Every selection combines: each
        chart re-aggregates over the rows passing all the <i>other</i> charts'
        filters (never its own), and the table below lists exactly those rows.
        Try brushing the fluctuation histogram and watch the outcome donut
        reshape. <b>Reset all filters</b> clears everything.
      </div>
    </div>
  </div>
</template>

<script>
import VueApexCharts from 'vue-apexcharts'
import ApexCharts from 'apexcharts'

import 'apexcharts/features/link'

// Deterministic trade records. outcome is derived from the fluctuation sign,
// so brushing the fluctuation histogram visibly reshapes the outcome donut.
function tradesData() {
  var quarters = ['Q1', 'Q2', 'Q3', 'Q4']
  var days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
  var out = []
  for (var i = 0; i < 120; i++) {
    var pct =
      3.0 * Math.sin(i / 5.0) +
      1.3 * Math.sin(i / 1.7) +
      0.4 * Math.cos(i / 3.1)
    var vol = Math.round(120 + 70 * Math.sin(i / 6.0) + 40 * Math.cos(i / 2.3))
    out.push({
      id: i + 1,
      quarter: quarters[Math.floor(i / 30) % 4],
      day: days[(i * 3) % 5],
      outcome: pct >= 0 ? 'Gain' : 'Loss',
      pct: Math.round(pct * 100) / 100,
      volume: Math.max(20, vol),
    })
  }
  return out
}

// Register the shared record set BEFORE the charts are constructed.
ApexCharts.crossfilter({ id: 'trades', records: tradesData() })

export default {
  components: {
    apexchart: VueApexCharts,
  },
  data: function () {
    return {
      series: [],
      chartOptions: {
        chart: {
          id: 'byQuarter',
          type: 'donut',
          height: 260,
          fontFamily: 'Helvetica, Arial, sans-serif',
          animations: { speed: 450 },
          link: {
            id: 'trades',
            dimension: function (r) {
              return r.quarter
            },
            reduce: 'count',
            dimOpacity: 0.18,
          },
        },
        title: { text: 'Quarter', align: 'left' },
        legend: { position: 'bottom', fontSize: '12px' },
        plotOptions: { pie: { expandOnClick: false } },
        dataLabels: {
          enabled: true,
          formatter: function (v, o) {
            return o.w.config.series[o.seriesIndex]
          },
        },
        colors: ['#2563EB', '#0ea5e9', '#22c55e', '#f59e0b'],
      },
      series1: [],
      chartOptions1: {
        chart: {
          id: 'byOutcome',
          type: 'donut',
          height: 260,
          fontFamily: 'Helvetica, Arial, sans-serif',
          animations: { speed: 450 },
          link: {
            id: 'trades',
            dimension: function (r) {
              return r.outcome
            },
            reduce: 'count',
            dimOpacity: 0.18,
          },
        },
        title: { text: 'Outcome', align: 'left' },
        legend: { position: 'bottom', fontSize: '12px' },
        plotOptions: { pie: { expandOnClick: false } },
        dataLabels: {
          enabled: true,
          formatter: function (v, o) {
            return o.w.config.series[o.seriesIndex]
          },
        },
        colors: ['#22c55e', '#ef4444'],
      },
      series2: [],
      chartOptions2: {
        chart: {
          id: 'byDay',
          type: 'bar',
          height: 260,
          fontFamily: 'Helvetica, Arial, sans-serif',
          animations: { speed: 450 },
          link: {
            id: 'trades',
            dimension: function (r) {
              return r.day
            },
            reduce: 'count',
            seriesName: 'Trades',
            dimOpacity: 0.18,
          },
        },
        title: { text: 'Day of week', align: 'left' },
        plotOptions: {
          bar: { columnWidth: '55%', borderRadius: 3, distributed: true },
        },
        legend: { show: false },
        dataLabels: { enabled: false },
        colors: ['#2563EB', '#0ea5e9', '#22c55e', '#f59e0b', '#F43F5E'],
      },
      series3: [],
      chartOptions3: {
        chart: {
          id: 'byFluctuation',
          type: 'bar',
          height: 240,
          fontFamily: 'Helvetica, Arial, sans-serif',
          animations: { speed: 400 },
          zoom: { enabled: false },
          selection: { enabled: true },
          toolbar: {
            autoSelected: 'selection',
            tools: {
              selection: true,
              zoom: false,
              pan: false,
              reset: false,
              download: false,
            },
          },
          link: {
            id: 'trades',
            dimension: function (r) {
              return r.pct
            },
            bins: { count: 26 },
            dimOpacity: 0.16,
          },
        },
        title: { text: 'Fluctuation % (brush a range)', align: 'left' },
        plotOptions: { bar: { columnWidth: '92%' } },
        dataLabels: { enabled: false },
        xaxis: {
          type: 'numeric',
          tickAmount: 8,
          labels: {
            formatter: function (v) {
              return Number(v).toFixed(1)
            },
          },
        },
        colors: ['#2563EB'],
      },
      series4: [],
      chartOptions4: {
        chart: {
          id: 'byVolume',
          type: 'bar',
          height: 240,
          fontFamily: 'Helvetica, Arial, sans-serif',
          animations: { speed: 400 },
          zoom: { enabled: false },
          selection: { enabled: true },
          toolbar: {
            autoSelected: 'selection',
            tools: {
              selection: true,
              zoom: false,
              pan: false,
              reset: false,
              download: false,
            },
          },
          link: {
            id: 'trades',
            dimension: function (r) {
              return r.volume
            },
            bins: { count: 22 },
            dimOpacity: 0.16,
          },
        },
        title: { text: 'Volume (brush a range)', align: 'left' },
        plotOptions: { bar: { columnWidth: '92%' } },
        dataLabels: { enabled: false },
        xaxis: {
          type: 'numeric',
          tickAmount: 8,
          labels: {
            formatter: function (v) {
              return Math.round(v)
            },
          },
        },
        colors: ['#0ea5e9'],
      },
      cfTrades: null,
    }
  },
  mounted: function () {
    // Five linked views (byQuarter..byVolume) + a live data table, all over the
    // shared 'trades' record set (registered in the head script). The wrapper owns
    // the render, so poll until every chart instance and the crossfilter exist,
    // then wire the table + readout + reset exactly as the vanilla build does.
    var me = this
    me._onReset = function () {
      if (me.cfTrades) me.cfTrades.reset()
    }
    var timer = window.setInterval(function () {
      var cf = ApexCharts.getCrossfilter('trades')
      var ready =
        cf &&
        ApexCharts.getChartByID('byQuarter') &&
        ApexCharts.getChartByID('byOutcome') &&
        ApexCharts.getChartByID('byDay') &&
        ApexCharts.getChartByID('byFluctuation') &&
        ApexCharts.getChartByID('byVolume')
      if (!ready) return
      window.clearInterval(timer)
      me._timer = null
      me.cfTrades = cf

      var readout = document.getElementById('cfd-readout')

      // Bind the data table to the filtered rows; it re-renders on every change.
      me._table = cf.dataTable(document.getElementById('cfd-table'), {
        columns: [
          { field: 'id', label: '#' },
          { field: 'quarter', label: 'Quarter' },
          { field: 'day', label: 'Day' },
          { field: 'outcome', label: 'Outcome' },
          {
            field: 'pct',
            label: 'Fluctuation %',
            format: function (v) {
              return v.toFixed(2) + '%'
            },
          },
          { field: 'volume', label: 'Volume' },
        ],
      })

      me._offChange = cf.on('change', function (state) {
        var ids = Object.keys(state.filters)
        var label = ids.length ? ids.join(', ') : 'none'
        readout.textContent =
          state.filteredCount +
          ' / ' +
          state.total +
          ' trades   (active: ' +
          label +
          ')'
      })

      // Seed the readout with the unfiltered totals.
      var s = cf.state()
      readout.textContent =
        s.filteredCount + ' / ' + s.total + ' trades   (active: none)'

      document
        .getElementById('cfd-reset')
        .addEventListener('click', me._onReset)
    }, 50)
    me._timer = timer
  },
  beforeDestroy: function () {
    if (this._timer) window.clearInterval(this._timer)
    if (this._offChange) this._offChange()
    if (this._table) this._table.destroy()
    var btn = document.getElementById('cfd-reset')
    if (btn && this._onReset) btn.removeEventListener('click', this._onReset)
  },
}
</script>

<style>
.cfd {
  max-width: 700px;
  margin: 10px auto;
  font-family: Helvetica, Arial, sans-serif;
  color: #1f2430;
}
.cfd-bar {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
  margin: 4px 6px 12px;
}
.cfd-bar button {
  padding: 6px 13px;
  cursor: pointer;
  border: 1px solid #2563eb;
  color: #4338ca;
  background: #fff;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 600;
}
.cfd-bar .readout {
  font-family: monospace;
  font-size: 13px;
  color: #445;
}
.cfd-grid {
  display: grid;
  /* Auto-fit reflows the three categorical charts from 3 columns down to 2,
       then 1, as the container narrows, so they never get crushed at ~700px. */
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 8px;
}
.cfd-card {
  border: 1px solid #eef;
  border-radius: 8px;
  padding: 6px;
}
/* The two brush histograms always take a full row, whatever the column count. */
.cfd-card.brush {
  grid-column: 1 / -1;
}
.cfd-tablewrap {
  margin: 12px 6px;
  max-height: 280px;
  overflow: auto;
  border: 1px solid #eef;
  border-radius: 8px;
}
.cfd-note {
  background: #eef2ff;
  border-left: 3px solid #2563eb;
  padding: 11px 15px;
  font-size: 13px;
  color: #234;
  border-radius: 2px;
  line-height: 1.6;
  margin: 12px 6px;
}
.cfd-note code {
  background: #dfe3ff;
  padding: 1px 5px;
  border-radius: 3px;
}
</style>