// 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()
})