import React from 'react'
import ReactApexChart from 'react-apexcharts'
import ApexCharts from 'apexcharts'
import './styles.css'
// 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() })
const ApexChart = () => {
const [state, setState] = React.useState({
series: [],
options: {
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: [],
options1: {
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: [],
options2: {
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: [],
options3: {
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: [],
options4: {
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'],
},
})
React.useEffect(() => {
// 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.
let cf
let table
let offChange
const onReset = () => {
if (cf) cf.reset()
}
const timer = window.setInterval(() => {
cf = ApexCharts.getCrossfilter('trades')
const ready =
cf &&
ApexCharts.getChartByID('byQuarter') &&
ApexCharts.getChartByID('byOutcome') &&
ApexCharts.getChartByID('byDay') &&
ApexCharts.getChartByID('byFluctuation') &&
ApexCharts.getChartByID('byVolume')
if (!ready) return
window.clearInterval(timer)
const readout = document.getElementById('cfd-readout')
// Bind the data table to the filtered rows; it re-renders on every change.
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: (v) => v.toFixed(2) + '%',
},
{ field: 'volume', label: 'Volume' },
],
})
offChange = cf.on('change', (state) => {
const ids = Object.keys(state.filters)
const label = ids.length ? ids.join(', ') : 'none'
readout.textContent =
state.filteredCount +
' / ' +
state.total +
' trades (active: ' +
label +
')'
})
// Seed the readout with the unfiltered totals.
const s = cf.state()
readout.textContent =
s.filteredCount + ' / ' + s.total + ' trades (active: none)'
document.getElementById('cfd-reset').addEventListener('click', onReset)
}, 50)
return () => {
window.clearInterval(timer)
if (offChange) offChange()
if (table) table.destroy()
const btn = document.getElementById('cfd-reset')
if (btn) btn.removeEventListener('click', onReset)
}
}, [])
return (
<div>
<div className="cfd">
<div className="cfd-bar">
<button id="cfd-reset">Reset all filters</button>
<span className="readout" id="cfd-readout"></span>
</div>
<div className="cfd-grid">
<div className="cfd-card">
<div id="chart">
<ReactApexChart
options={state.options}
series={state.series}
type="donut"
height={260}
/>
</div>
</div>
<div className="cfd-card">
<div id="chart2">
<ReactApexChart
options={state.options1}
series={state.series1}
type="donut"
height={260}
/>
</div>
</div>
<div className="cfd-card">
<div id="chart3">
<ReactApexChart
options={state.options2}
series={state.series2}
type="bar"
height={260}
/>
</div>
</div>
<div className="cfd-card brush">
<div id="chart4">
<ReactApexChart
options={state.options3}
series={state.series3}
type="bar"
height={240}
/>
</div>
</div>
<div className="cfd-card brush">
<div id="chart5">
<ReactApexChart
options={state.options4}
series={state.series4}
type="bar"
height={240}
/>
</div>
</div>
</div>
<div className="cfd-tablewrap" id="cfd-table"></div>
<div className="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>
)
}
export default ApexChart