<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'

// 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>
Crossfilter Dashboard - Vue Interactivity | ApexCharts.js | ApexCharts.js