// A year at Halo, a (fictional) coffee roaster: 2,400 kilos roasted, one dot
// per kilo, told in five beats on ONE unit chart. Halo's logo is not in the
// shape catalog and never will be, because it is Halo's. It does not need to
// be: `shapeFrom` takes an outline and packs it with the same engine the
// built-in shapes use, so any company's mark is a layout the moment someone
// pastes its path data. These definitions are shared by the vanilla-js, React
// and Vue builds.

// The mark: a cup on a saucer under two rising wisps, drawn as eight subpaths
// in a 0-100 box.
//
// This is the DEFAULT nonzero fill rule doing the two things that make a mark
// like this authorable in pieces rather than as one traced contour:
//
//   1. Overlapping subpaths UNION. The cup, its elliptical rim, its foot, the
//      saucer and the handle are five separate shapes that simply overlap, and
//      they merge into one silhouette. Nothing has to be mitred or joined by
//      hand, which is why a logo can be assembled part by part.
//   2. A subpath wound the OTHER WAY cuts a hole. The handle is a solid loop
//      with the finger hole punched back out of it, so the hole is not a
//      separate idea from the fill: it is the same ellipse, wound backwards.
//
// The winding is the thing to get right and the thing nothing in the path data
// reveals. Here every union subpath is written clockwise on screen, and the one
// hole runs anticlockwise (as an arc, `sweep 1` is the positive direction and
// `sweep 0` the negative one). A subpath wound backwards by accident still
// renders, so it looks deliberate: a cup with its body bored through.
var HALO_PATH =
  // the cup: straight tapering sides down to a rounded bottom
  'M 72 42 L 62 72 C 62 76, 55 77, 46 77 C 37 77, 30 76, 30 72 L 20 42 Z ' +
  // the rim, an ellipse overlapping the cup's flat top so it reads as a vessel
  // seen slightly from above
  'M 20 42 A 26 7 0 0 1 72 42 A 26 7 0 0 1 20 42 Z ' +
  // the foot
  'M 38 75 L 54 75 L 57 81.5 L 35 81.5 Z ' +
  // the saucer
  'M 4 86.5 A 42 6.5 0 0 1 88 86.5 A 42 6.5 0 0 1 4 86.5 Z ' +
  // the handle: a solid loop overlapping the cup's right wall...
  'M 64 58 A 14 11 0 0 1 92 58 A 14 11 0 0 1 64 58 Z ' +
  // ...and the finger hole, the SAME kind of ellipse wound the other way
  // (`sweep 0`), which is what turns it from a blob into a handle
  'M 74 58 A 7 5 0 0 0 88 58 A 7 5 0 0 0 74 58 Z ' +
  // two wisps of steam, each tapering to a point at both ends
  'M 48 37 C 36 27, 56 19, 47 5 C 50 4, 52 5, 54 7 C 60 19, 44 26, 54 37 ' +
  'C 52 38, 50 38, 48 37 Z ' +
  'M 30 36 C 21 28, 35 21, 28 9 C 30 8, 32 9, 34 11 C 39 21, 28 27, 36 36 ' +
  'C 34 37, 32 37, 30 36 Z'

var halo = ApexUnitShapes.shapeFrom(HALO_PATH, {
  name: 'halo',
  // Measured, not guessed: at 600 dots the steam has thinned to a single file
  // and the finger hole is one dot from closing, which is the floor. Ask for
  // fewer and the chart warns in the console rather than drawing mush. Every
  // logo has a number like this; finding yours is the one piece of homework a
  // custom shape asks for.
  minUnits: 600,
})

// The same outline, filled from the middle out instead of top down. Nothing is
// re-authored: `.with()` returns a variant and leaves the original alone.
var haloCore = halo.with({ order: 'centerOut' })

// The dots spelling out how many of them there are. `glyphs` builds a
// seven-segment centreline from the text and thickens it, so this is a stroke
// shape and needs no outline at all.
var total = ApexUnitShapes.glyphs('2,400')

var LINES = ['Espresso', 'Filter', 'Decaf', 'Cold brew']
var KILOS = [980, 720, 300, 400] // 2,400
var ROAST = ['#5C3A21', '#C77B30', '#6F9B4A', '#2E8B9A']

// A bare ViewState shared by every beat: the unit chart has no axes to window,
// so each beat only needs to describe its own layout in `options`. The series,
// labels and colours never change across this story: one crowd, five
// arrangements.
var LIGHT = { theme: { mode: 'light' } }

function beat(unit) {
  return {
    series: KILOS,
    labels: LINES,
    colors: ROAST,
    // 'flow' keys the dots by global order, so the SAME dot glides from one
    // arrangement into the next instead of being rebuilt. It is the whole
    // reason this reads as one crowd moving rather than five charts.
    plotOptions: { unit: Object.assign({ transition: 'flow' }, unit) },
  }
}

var BEATS = [
  {
    selector: '#lg-step-1',
    view: LIGHT,
    options: beat({ layout: 'packed' }),
    announce: 'A year of roasting: 2,400 kilos in one packed crowd',
  },
  {
    selector: '#lg-step-2',
    view: LIGHT,
    options: beat({ layout: 'custom', positions: halo }),
    announce:
      'The same 2,400 kilos, gathered into the company mark: a cup of coffee',
  },
  {
    selector: '#lg-step-3',
    view: LIGHT,
    options: beat({ layout: 'custom', positions: haloCore }),
    announce: 'Refilled from the centre out: espresso fills the cup',
  },
  {
    selector: '#lg-step-4',
    view: LIGHT,
    options: beat({ layout: 'columns' }),
    announce: 'The same kilos as four plain columns, in true proportion',
  },
  {
    selector: '#lg-step-5',
    view: LIGHT,
    options: beat({ layout: 'custom', positions: total }),
    announce: 'The dots spell out their own total: 2,400',
  },
]

var options = {
  series: [980, 720, 300, 400],
  chart: {
    id: 'haloStory',
    type: 'unit',
    height: 420,
    fontFamily: 'Helvetica, Arial, sans-serif',
    animations: {
      enabled: true,
      speed: 850,
    },
    toolbar: { show: false },
  },
  labels: ['Espresso', 'Filter', 'Decaf', 'Cold brew'],
  colors: ['#5C3A21', '#C77B30', '#6F9B4A', '#2E8B9A'],
  legend: {
    position: 'bottom',
  },
  plotOptions: {
    unit: {
      layout: 'packed',
      transition: 'flow',
      // A fixed dot size keeps every dot the SAME size in all five layouts, so
      // dots only move across the story and never resize.
      //
      // Which beat sets the number is worth knowing, because it is not the one it
      // looks like. A silhouette fits the dot gap to its OWN area and then clamps
      // the radius to that gap, so the cup ignores anything above ~1.7 and beats
      // 2, 3 and 5 render identically at 1.7 or at 3. The binding constraint is
      // beat 1: the packed blob grows with the size it is given, and at 2 it
      // reaches 17px past the top of the legend.
      size: 1.7,
      spacing: 1.05,
      clusterLabels: {
        show: false,
      },
    },
  },
}

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

// beatChange fires on every activation (scroll or goTo): drive the page UI.
// (The shape, the beats and the data live in the shared head script.)
var steps = document.querySelectorAll('.lg-step')
var dots = document.querySelectorAll('.lg-dot')
var chip = document.getElementById('lg-chip')
var prevBtn = document.getElementById('lg-prev')
var nextBtn = document.getElementById('lg-next')
var scroller = document.getElementById('lg-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('lg-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: '#lg-scroller' })
})