// One field of 600 marathon finishers, shown three ways on a single unit chart:
// as five regional clusters, as one packed field, then regrouped into six
// finish-time bars. The design rule that makes it a story rather than three
// separate charts: every beat sets plotOptions.unit.transition:'flow', so the
// SAME dots are keyed by global order and GLIDE (and recolour) from one
// arrangement into the next instead of being rebuilt. These definitions are
// shared by the vanilla-js, React and Vue builds.
var REGION_LABELS = ['Africa', 'Europe', 'Americas', 'Asia', 'Oceania']
var REGION_COLORS = ['#F2994A', '#2D9CDB', '#27AE60', '#EB5757', '#9B51E0']
var REGION_COUNT = [100, 170, 150, 140, 40] // 600 finishers
var FINISH_LABELS = [
'Sub-3:00',
'3:00-3:30',
'3:30-4:00',
'4:00-4:30',
'4:30-5:00',
'5:00+',
]
var FINISH_COLORS = [
'#1E8E5A',
'#35B37E',
'#86C82B',
'#F2C037',
'#F2994A',
'#EB5757',
]
var FINISH_COUNT = [45, 120, 165, 140, 85, 45] // also 600
// A bare ViewState shared by every beat: the unit chart has no axes to window,
// so each beat only needs to describe its own data + layout in `options`.
var LIGHT = { theme: { mode: 'light' } }
var BEATS = [
{
selector: '#sb-step-1',
view: LIGHT,
options: {
series: REGION_COUNT,
labels: REGION_LABELS,
colors: REGION_COLORS,
plotOptions: { unit: { layout: 'grouped', transition: 'flow' } },
},
announce: 'The field by region: five clusters of finishers',
},
{
selector: '#sb-step-2',
view: LIGHT,
options: {
series: REGION_COUNT,
labels: REGION_LABELS,
colors: REGION_COLORS,
plotOptions: { unit: { layout: 'packed', transition: 'flow' } },
},
announce:
'One field: the same finishers packed together, smallest region centred',
},
{
selector: '#sb-step-3',
view: LIGHT,
options: {
series: FINISH_COUNT,
labels: FINISH_LABELS,
colors: FINISH_COLORS,
plotOptions: { unit: { layout: 'columns', transition: 'flow' } },
},
announce: 'Regrouped by finish time: the finishers flow into six bars',
},
]
var options = {
series: [100, 170, 150, 140, 40],
chart: {
id: 'marathonStory',
type: 'unit',
height: 420,
fontFamily: 'Helvetica, Arial, sans-serif',
animations: {
enabled: true,
speed: 850,
},
toolbar: { show: false },
},
labels: ['Africa', 'Europe', 'Americas', 'Asia', 'Oceania'],
colors: ['#F2994A', '#2D9CDB', '#27AE60', '#EB5757', '#9B51E0'],
legend: {
position: 'bottom',
},
plotOptions: {
unit: {
layout: 'grouped',
transition: 'flow',
// A fixed dot size keeps every dot the SAME size in all three layouts, so
// dots only move and recolour across the story (they never resize). The
// size is set by the tightest layout: 600 dots must fit the packed field
// and the widest region cluster, so the dots are small on purpose.
size: 2.3,
spacing: 1.15,
},
},
}
var chart = new ApexCharts(document.querySelector('#chart'), options)
chart.render()
// beatChange fires on every activation (scroll or goTo): drive the page UI.
// (The labels, colors, counts and BEATS live in the shared head script.)
var steps = document.querySelectorAll('.sb-step')
var dots = document.querySelectorAll('.sb-dot')
var chip = document.getElementById('sb-chip')
var prevBtn = document.getElementById('sb-prev')
var nextBtn = document.getElementById('sb-next')
var scroller = document.getElementById('sb-scroller')
var current = 0
// Center a beat's step inside the story column WITHOUT scrolling the page:
// adjust only the panel's own scrollTop. The IntersectionObserver then
// activates that beat, so the manual controls and the scroll stay in sync.
function scrollToBeat(i) {
var step = document.getElementById('sb-step-' + (i + 1))
if (!step || !scroller) return
var sRect = step.getBoundingClientRect()
var cRect = scroller.getBoundingClientRect()
scroller.scrollTop +=
sRect.top - cRect.top - (scroller.clientHeight - step.clientHeight) / 2
}
function goToBeat(i) {
i = Math.max(0, Math.min(BEATS.length - 1, i))
chart.storyboard.goTo(i)
scrollToBeat(i)
}
chart.addEventListener('beatChange', function (c, info) {
current = info.index
steps.forEach(function (el, i) {
el.classList.toggle('is-active', i === info.index)
})
dots.forEach(function (dot, i) {
dot.classList.toggle('is-active', i === info.index)
})
chip.textContent = 'Beat ' + (info.index + 1) + ' of ' + BEATS.length
prevBtn.disabled = info.index === 0
nextBtn.disabled = info.index === BEATS.length - 1
})
dots.forEach(function (dot, i) {
dot.addEventListener('click', function () {
goToBeat(i)
})
})
prevBtn.addEventListener('click', function () {
goToBeat(current - 1)
})
nextBtn.addEventListener('click', function () {
goToBeat(current + 1)
})
window.addEventListener('load', function () {
// scroller binds the observer to the story column, so the story is driven by
// that panel's own scroll (not the page/iframe viewport).
chart.storyboard.bind({ beats: BEATS, scroller: '#sb-scroller' })
})