import React from 'react'
import ReactApexChart from 'react-apexcharts'
import ApexCharts from 'apexcharts'
import './styles.css'

// 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',
  },
]

const ApexChart = () => {
  const [state, setState] = React.useState({
    series: [
      {
        name: 'Revenue (k$)',
        data: [12, 14, 9, 15, 18, 22, 26, 31, 37, 44, 52, 61],
      },
    ],
    options: {
      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',
        ],
      },
    },
  })

  React.useEffect(() => {
    // The react-apexcharts wrapper owns the render, so reach the live instance by
    // its chart.id. Poll until it exists, then wire the same story the vanilla
    // build does. (REV, MONTHS, the palettes and BEATS live in the shared head
    // script.) The storyboard drives the chart instance directly, so no React
    // state changes and the <ReactApexChart> is never re-rendered under it.
    let chart
    const timer = window.setInterval(() => {
      chart = ApexCharts.getChartByID('storyChart')
      if (!chart) return
      window.clearInterval(timer)

      const steps = document.querySelectorAll('.sb-step')
      const dots = document.querySelectorAll('.sb-dot')
      const chip = document.getElementById('sb-chip')
      const prevBtn = document.getElementById('sb-prev')
      const nextBtn = document.getElementById('sb-next')
      const scroller = document.getElementById('sb-scroller')
      let 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 controls and the scroll stay in sync.
      const scrollToBeat = (i) => {
        const step = document.getElementById('sb-step-' + (i + 1))
        if (!step || !scroller) return
        const sRect = step.getBoundingClientRect()
        const cRect = scroller.getBoundingClientRect()
        scroller.scrollTop +=
          sRect.top -
          cRect.top -
          (scroller.clientHeight - step.clientHeight) / 2
      }
      const goToBeat = (i) => {
        i = Math.max(0, Math.min(BEATS.length - 1, i))
        chart.storyboard.goTo(i)
        scrollToBeat(i)
      }

      chart.addEventListener('beatChange', (c, info) => {
        current = info.index
        steps.forEach((el, i) =>
          el.classList.toggle('is-active', i === info.index),
        )
        dots.forEach((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((dot, i) => {
        dot.addEventListener('click', () => goToBeat(i))
      })
      prevBtn.addEventListener('click', () => goToBeat(current - 1))
      nextBtn.addEventListener('click', () => goToBeat(current + 1))

      // 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' })
    }, 50)

    return () => {
      window.clearInterval(timer)
      if (chart && chart.storyboard) chart.storyboard.unbind()
    }
  }, [])

  return (
    <div>
      <div className="sb-wrap">
        <div className="sb-hero">
          <h1>One year, one chart</h1>
          <p>
            A scrollytelling story on a single live chart. Scroll the story and
            the same twelve columns recolor, annotate, and finally reshape;
            scrolling back up rewinds the story.
          </p>
        </div>

        <div className="sb-card">
          <div className="sb-layout">
            <div className="sb-scroller" id="sb-scroller">
              <div className="sb-steps">
                <section className="sb-step" id="sb-step-1">
                  <div className="card">
                    <h3>One year of revenue</h3>
                    <p>
                      Fiscal 2025, month by month: 341 k$ across twelve columns.
                      A slow start and a strong finish, with two turning points
                      hiding in plain sight.
                    </p>
                  </div>
                </section>
                <section className="sb-step" id="sb-step-2">
                  <div className="card">
                    <h3>March: the outage</h3>
                    <p>
                      A bad deploy took sign-ups offline for two days, and March
                      is the only month that broke the climb. The spotlight
                      narrows.
                    </p>
                  </div>
                </section>
                <section className="sb-step" id="sb-step-3">
                  <div className="card">
                    <h3>May: v2 ships</h3>
                    <p>
                      The hinge of the year. From May on, every month tops the
                      last, and the slope never looks back.
                    </p>
                  </div>
                </section>
                <section className="sb-step" id="sb-step-4">
                  <div className="card">
                    <h3>The quarters take shape</h3>
                    <p>
                      Same columns, new grouping. Each quarter beats the last by
                      half again or more: 35 k$ in Q1 grows to 157 k$ by Q4.
                    </p>
                  </div>
                </section>
                <section className="sb-step" id="sb-step-5">
                  <div className="card">
                    <h3>The year in one circle</h3>
                    <p>
                      The twelve months curl into a ring, each column becoming
                      its own slice. The quarter colors survive the change of
                      shape.
                    </p>
                  </div>
                </section>
              </div>
            </div>

            <div className="sb-graphic">
              <div id="chart">
                <ReactApexChart
                  options={state.options}
                  series={state.series}
                  type="bar"
                  height={300}
                />
              </div>

              <div className="sb-head">
                <div>
                  <b>FY25 revenue, monthly</b>
                  <span className="sb-chip" id="sb-chip">
                    Beat 1 of 5
                  </span>
                </div>
                <div className="sb-nav">
                  <button
                    className="sb-btn"
                    id="sb-prev"
                    type="button"
                    disabled
                  >
                    Prev
                  </button>
                  <button
                    className="sb-dot is-active"
                    aria-label="Go to beat 1"
                  ></button>
                  <button className="sb-dot" aria-label="Go to beat 2"></button>
                  <button className="sb-dot" aria-label="Go to beat 3"></button>
                  <button className="sb-dot" aria-label="Go to beat 4"></button>
                  <button className="sb-dot" aria-label="Go to beat 5"></button>
                  <button className="sb-btn" id="sb-next" type="button">
                    Next
                  </button>
                </div>
              </div>
            </div>
          </div>
        </div>

        <p className="sb-hint">
          Scroll the story, or use the beat dots and Prev/Next. Each beat is a
          saved view; scrolling back up reverses it.
        </p>

        <div className="sb-note">
          <code>chart.storyboard.bind(&#123; beats, scroller &#125;)</code>{' '}
          pairs prose elements with views; here <code>scroller</code> points the
          observer at the story column, so beats apply as their step crosses the
          middle of that panel (IntersectionObserver, no scroll listeners) and
          re-apply in reverse on the way up. The design rule that keeps every
          transition smooth: all five beats keep the <b>same twelve marks</b>.
          Emphasis moves by color (spotlights, quarter groups) and annotations,
          never by cropping or re-binning, so nothing pops in or out. The
          finale's <code>options</code> payload swaps
          <code>chart.type</code>, and the <code>morph</code> feature curls each
          column into its own donut slice, a one-to-one morph. The dots and
          Prev/Next call
          <code>chart.storyboard.goTo(i)</code> and scroll the story to match,
          each beat announces itself to the aria-live region, and{' '}
          <code>beatChange</code>
          drives the step highlight. Transitions are cut instead of animated
          under prefers-reduced-motion. Needs the <code>storyboard</code>{' '}
          feature (bundles
          <code>perspectives</code>).
        </div>
      </div>
    </div>
  )
}

export default ApexChart
Scrollytelling (Storyboard) - React Narrative & State | ApexCharts.js | ApexCharts.js