import React from 'react'
import ReactApexChart from 'react-apexcharts'
import ApexCharts from 'apexcharts'
import './styles.css'
// Shared by the vanilla-js, React and Vue builds.
//
// 640 delivery times, seeded so the page is deterministic. The series carries
// the RAW OBSERVATIONS: the chart does the binning, which is what lets it hand
// them back again later.
var DELIVERIES = (function () {
var seed = 20260814
function rand() {
seed = (seed * 16807) % 2147483647
return (seed - 1) / 2147483646
}
var out = []
for (var i = 0; i < 640; i++) {
var u1 = Math.max(rand(), 1e-9)
var u2 = rand()
var z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2)
// Log-normal: a delivery can run very late but never finish early.
out.push(Math.round(Math.exp(3.15 + z * 0.42)))
}
return out
})()
// The histogram's own series, kept so the chart can be put back together.
var HISTOGRAM_SERIES = [{ name: 'Deliveries', data: DELIVERIES }]
// Quick (blue) to punctual (grey-green) to very late (amber, then red).
var RAMP = ['#4e8cff', '#59c2b0', '#e8c14a', '#ef7d3a', '#d94040']
function rampColor(t) {
var scaled = Math.max(0, Math.min(1, t)) * (RAMP.length - 1)
var i = Math.min(RAMP.length - 2, Math.floor(scaled))
var f = scaled - i
var a = RAMP[i]
var b = RAMP[i + 1]
var mix = function (o) {
var av = parseInt(a.substr(o, 2), 16)
var bv = parseInt(b.substr(o, 2), 16)
var v = Math.round(av + (bv - av) * f)
return (v < 16 ? '0' : '') + v.toString(16)
}
return '#' + mix(1) + mix(3) + mix(5)
}
function setReadout(text) {
var el = document.querySelector('#readout')
if (el) el.innerHTML = text
}
function setActive(exploded) {
var buttons = [].slice.call(document.querySelectorAll('[data-explode]'))
buttons.forEach(function (b) {
b.className =
(b.getAttribute('data-explode') === 'true') === exploded ? 'on' : ''
})
}
// Wires the two buttons to a live chart. Shared by all three builds.
function wireExplode(chart) {
var buttons = [].slice.call(document.querySelectorAll('[data-explode]'))
buttons.forEach(function (b) {
b.addEventListener('click', function () {
// The active view's button is a no-op: re-requesting the readings while
// already exploded would ask rowSeries() of a unit chart, which has no
// rows to hand back.
if (b.className === 'on') return
var explode = b.getAttribute('data-explode') === 'true'
setActive(explode)
if (explode) {
// The whole point: nothing about the sample is passed in here. The
// chart already knows which observations it counted into each bar, so
// rowSeries() returns one cluster per bar holding exactly those rows.
var rows = chart.rowSeries()
var total = rows.reduce(function (n, c) {
return n + c.data.length
}, 0)
// What comes back is ordinary series data, so it can be decorated.
// Colouring each bar's dots by how late that bar was keeps the
// distribution readable once the bars are gone: without this the blob
// is 640 identical dots and you cannot tell a quick delivery from a
// disastrous one.
rows.forEach(function (cluster, k) {
var color = rampColor(rows.length > 1 ? k / (rows.length - 1) : 0)
cluster.data.forEach(function (d) {
d.fillColor = color
})
})
chart.updateOptions({
chart: { type: 'unit' },
series: rows,
plotOptions: {
unit: {
// NOT 'columns'. That layout would stack each bin's dots back
// into a column the same height and place as the bar they left,
// so the objects would travel a few pixels and the whole thing
// would look like a redraw. 'packed' gathers them into one blob,
// which is a real journey.
layout: 'packed',
unitValue: 1,
size: 3,
},
},
legend: { show: false },
})
setReadout(
'One dot per delivery: <b>' +
total +
'</b> of them, ' +
'gathered out of <b>' +
rows.length +
'</b> bars.',
)
} else {
chart.updateOptions({
chart: { type: 'histogram' },
series: HISTOGRAM_SERIES,
legend: { show: false },
})
setReadout(
'Every bar is a count of the deliveries that landed in its range.',
)
}
})
})
setActive(false)
setReadout('Every bar is a count of the deliveries that landed in its range.')
}
const ApexChart = () => {
const [state, setState] = React.useState({
series: HISTOGRAM_SERIES,
options: {
chart: {
id: 'explodeHist',
type: 'histogram',
height: 420,
toolbar: {
show: false,
},
animations: {
chartTypeMorph: {
speed: 900,
},
},
},
plotOptions: {
histogram: {
bins: 22,
},
},
colors: ['#4e8cff'],
legend: {
show: false,
},
xaxis: {
title: {
text: 'Delivery time (minutes)',
},
labels: {
formatter: function (val) {
return Math.round(val)
},
},
},
yaxis: {
title: {
text: 'Deliveries',
},
},
},
})
React.useEffect(() => {
// The react-apexcharts wrapper owns the render, so reach the live instance
// by its chart.id before wiring the controls.
const timer = window.setInterval(() => {
const chart = ApexCharts.getChartByID('explodeHist')
if (!chart) return
window.clearInterval(timer)
wireExplode(chart)
}, 50)
return () => window.clearInterval(timer)
}, [])
return (
<div>
<div className="wrap">
<h1>What is actually inside a bar?</h1>
<p>
A histogram bar is a count, and a count is a thing that has already
thrown away the rows it counted. Except here it has not: the series
was handed the raw delivery times and the chart did the binning
itself, so it can still name the observations behind every bar. Press
the button and each bar comes apart into exactly the deliveries it was
standing for, every dot leaving from the slice of the bar that was
counting it.
</p>
<div className="actions">
<button data-explode="false" className="on">
Histogram
</button>
<button data-explode="true">Show every delivery</button>
</div>
<div className="chart-wrap">
<div id="chart">
<ReactApexChart
options={state.options}
series={state.series}
type="histogram"
height={420}
/>
</div>
</div>
<div className="readout" id="readout"></div>
<div className="note">
The exploded view is built by <code>chart.rowSeries()</code>, which
returns one cluster per bar holding that bar's own observations. No
data is passed back in: a histogram is one of the few marks that can
name its rows, because it was given them in the first place. Requires
<code>apexcharts/features/stats</code> for the rows and
<code>apexcharts/features/morph</code> for the transition, both of
which the full bundle already includes.
</div>
</div>
</div>
)
}
export default ApexChart