// The design rule of this story: every beat keeps the SAME twelve marks.
// Emphasis changes via per-column colors and annotations (values, axes and
// mark count never change), so each transition tweens element-for-element
// instead of popping marks in and out. The finale is the one shape change:
// a 1:1 bar-to-donut morph, each column curling into its own slice.
// These definitions are shared by the vanilla-js, React and Vue builds.
var REV = [12, 14, 9, 15, 18, 22, 26, 31, 37, 44, 52, 61]
var MONTHS = [
  'Jan',
  'Feb',
  'Mar',
  'Apr',
  'May',
  'Jun',
  'Jul',
  'Aug',
  'Sep',
  'Oct',
  'Nov',
  'Dec',
]

var ACCENT = '#0EA5E9'
var FADE = '#e3e8f4' // out-of-spotlight months step back to this
var RED = '#f87171'
var QCOLORS = ['#0EA5E9', '#7dd3fc', '#4ade80', '#fbbf24'] // one per quarter

function colors12(fn) {
  var a = []
  for (var i = 0; i < 12; i++) a.push(fn(i))
  return a
}
var C_ALL = colors12(function () {
  return ACCENT
})
var C_OUTAGE = colors12(function (i) {
  if (i === 2) return RED // March
  return i >= 1 && i <= 3 ? ACCENT : FADE
})
var C_V2 = colors12(function (i) {
  return i >= 4 ? ACCENT : FADE
}) // May on
var C_QUARTERS = colors12(function (i) {
  return QCOLORS[Math.floor(i / 3)]
})

// The shared column payload; a beat only swaps the colors array. labels []
// resets the donut's month labels so they never leak into the axis mapping.
var COLUMN_OPTS = {
  chart: { type: 'bar' },
  series: [{ name: 'Revenue (k$)', data: REV }],
  labels: [],
  stroke: { width: 0, colors: undefined },
  plotOptions: {
    bar: { columnWidth: '58%', borderRadius: 3, distributed: true },
  },
  legend: { show: false },
}
function columnBeat(colors) {
  return Object.assign({}, COLUMN_OPTS, { colors: colors })
}

// The finale: same twelve values, same quarter colors, new shape. The morph
// feature pairs the 12 columns with the 12 slices.
var DONUT_OPTS = {
  chart: { type: 'donut' },
  series: REV,
  labels: MONTHS,
  colors: C_QUARTERS,
  stroke: { width: 2, colors: ['#fff'] },
  legend: { show: false },
  plotOptions: {
    pie: {
      expandOnClick: false,
      donut: {
        size: '68%',
        labels: {
          show: true,
          total: {
            show: true,
            label: 'FY25 revenue',
            formatter: function () {
              return '341 k$'
            },
          },
        },
      },
    },
  },
}

var BEATS = [
  {
    selector: '#sb-step-1',
    view: { window: { xaxis: null, yaxis: [null] }, theme: { mode: 'light' } },
    options: columnBeat(C_ALL),
    announce: 'Overview: fiscal 2025 revenue, month by month',
  },
  {
    selector: '#sb-step-2',
    view: {
      window: { xaxis: null, yaxis: [null] },
      theme: { mode: 'light' },
      annotations: {
        static: {
          xaxis: [
            {
              x: 'Mar',
              strokeDashArray: 4,
              borderColor: '#f87171',
              label: {
                text: 'The outage',
                borderColor: '#fca5a5',
                style: { color: '#7f1d1d', background: '#fee2e2' },
              },
            },
          ],
        },
      },
    },
    options: columnBeat(C_OUTAGE),
    announce: 'Spotlight on March: the outage month',
  },
  {
    selector: '#sb-step-3',
    view: {
      window: { xaxis: null, yaxis: [null] },
      theme: { mode: 'light' },
      annotations: {
        static: {
          xaxis: [
            {
              x: 'May',
              strokeDashArray: 4,
              borderColor: '#4ade80',
              label: {
                text: 'v2 ships',
                borderColor: '#86efac',
                style: { color: '#14532d', background: '#dcfce7' },
              },
            },
          ],
        },
      },
    },
    options: columnBeat(C_V2),
    announce: 'Spotlight from May on: v2 changes the slope',
  },
  {
    selector: '#sb-step-4',
    view: { window: { xaxis: null, yaxis: [null] }, theme: { mode: 'light' } },
    options: columnBeat(C_QUARTERS),
    announce: 'The months grouped into quarters by color',
  },
  {
    selector: '#sb-step-5',
    view: { window: { xaxis: null, yaxis: [null] }, theme: { mode: 'light' } },
    options: DONUT_OPTS,
    announce: 'The twelve months as a donut ring',
  },
]

var options = {
  series: [
    {
      name: 'Revenue (k$)',
      data: [12, 14, 9, 15, 18, 22, 26, 31, 37, 44, 52, 61],
    },
  ],
  chart: {
    id: 'storyChart',
    type: 'bar',
    height: 300,
    fontFamily: 'Helvetica, Arial, sans-serif',
    animations: { speed: 700, dynamicAnimation: { speed: 500 } },
    toolbar: { show: false },
    zoom: { enabled: false },
  },
  colors: [
    '#0EA5E9',
    '#0EA5E9',
    '#0EA5E9',
    '#0EA5E9',
    '#0EA5E9',
    '#0EA5E9',
    '#0EA5E9',
    '#0EA5E9',
    '#0EA5E9',
    '#0EA5E9',
    '#0EA5E9',
    '#0EA5E9',
  ],
  plotOptions: {
    bar: { columnWidth: '58%', borderRadius: 3, distributed: true },
  },
  stroke: { width: 0 },
  dataLabels: { enabled: false },
  legend: { show: false },
  grid: { borderColor: '#eef0f6' },
  xaxis: {
    categories: [
      'Jan',
      'Feb',
      'Mar',
      'Apr',
      'May',
      'Jun',
      'Jul',
      'Aug',
      'Sep',
      'Oct',
      'Nov',
      'Dec',
    ],
  },
}

var chart = new ApexCharts(document.querySelector('#chart'), options)
chart.render()

// beatChange fires on every activation (scroll or goTo): drive the page UI.
// (REV, MONTHS, the color palettes 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' })
})
Scrollytelling (Storyboard) - JavaScript Narrative & State | ApexCharts.js | ApexCharts.js