// 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' })
})
Marathon Storyboard - JavaScript Unit Charts | ApexCharts.js | ApexCharts.js