<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({ id, records })</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>