// Shared by the vanilla-js, React and Vue builds.
//
// Daily practice minutes for three plans, generated from a seeded RNG so the
// page renders identically on every load. Each lane hides something a violin
// cannot show: Free has a hard 30-minute cap (a wall of readings the estimate
// smooths into a bulge), Pro is the healthy case the violin was made for, and
// Trial has so few readings that its confident-looking curve stands on
// almost nothing.
var seed = 11
function rand() {
  seed = (seed * 16807) % 2147483647
  return (seed - 1) / 2147483646
}
function gauss() {
  var u1 = Math.max(rand(), 1e-9)
  var u2 = rand()
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2)
}
function logNormal(n, median, sigma, lo, hi) {
  var out = []
  for (var i = 0; i < n; i++) {
    var v = Math.exp(Math.log(median) + sigma * gauss())
    out.push(Math.round(Math.min(hi, Math.max(lo, v))))
  }
  return out
}

var PLANS = [
  // The cap: anything the distribution puts past 30 lands ON 30 exactly.
  { name: 'Free', color: '#12b3a8', values: logNormal(150, 24, 0.5, 3, 30) },
  { name: 'Pro', color: '#5a67d8', values: logNormal(170, 34, 0.45, 6, 105) },
  { name: 'Trial', color: '#e8890c', values: logNormal(14, 26, 0.55, 4, 95) },
]

var COLORS = PLANS.map(function (p) {
  return p.color
})

var VIOLIN_SERIES = [
  {
    name: 'Minutes',
    data: PLANS.map(function (p) {
      // Raw observations only: the library runs the density estimate.
      return { x: p.name, points: p.values }
    }),
  },
]

function setActive(exploded) {
  var buttons = [].slice.call(document.querySelectorAll('[data-explode]'))
  buttons.forEach(function (b) {
    b.className =
      (b.getAttribute('data-explode') === 'true') === exploded ? 'on' : ''
  })
}

function median(values) {
  var s = values.slice().sort(function (a, b) {
    return a - b
  })
  var m = (s.length - 1) / 2
  return (s[Math.floor(m)] + s[Math.ceil(m)]) / 2
}

function renderSummary() {
  var el = document.querySelector('#summary')
  if (!el) return
  var rows = PLANS.map(function (p) {
    var pinned = p.values.filter(function (v) {
      return v === 30
    }).length
    return (
      '<tr><td>' +
      p.name +
      '</td>' +
      '<td>' +
      p.values.length +
      '</td>' +
      '<td>' +
      median(p.values) +
      ' min</td>' +
      '<td>' +
      (p.name === 'Free' ? pinned : '-') +
      '</td></tr>'
    )
  })
  el.innerHTML =
    '<table><thead><tr><th>Plan</th><th>Readings</th><th>Median</th>' +
    '<th>Pinned at the 30 min cap</th></tr></thead><tbody>' +
    rows.join('') +
    '</tbody></table>'
}

// Wires the two buttons to a live chart. Shared by all three builds.
function wireExplode(chart) {
  var buttons = [].slice.call(document.querySelectorAll('[data-explode]'))

  buttons.forEach(function (b) {
    b.addEventListener('click', function () {
      // The active view's button is a no-op: re-requesting the readings while
      // already exploded would ask rowSeries() of a unit chart, which has no
      // rows to hand back.
      if (b.className === 'on') return
      var explode = b.getAttribute('data-explode') === 'true'
      setActive(explode)

      if (explode) {
        // The violins were estimated from the observations, so the chart can
        // hand each violin's own readings back: every dot leaves from the
        // curve it was smoothed into.
        var rows = chart.rowSeries()
        // rowSeries() colours by series, and this violin is ONE series split
        // across three lanes (distributed). Re-key the colour by lane so each
        // violin's ink keeps its own colour on the way out.
        rows.forEach(function (cluster, k) {
          cluster.data.forEach(function (d) {
            d.fillColor = COLORS[k]
          })
        })
        chart.updateOptions({
          chart: { type: 'unit' },
          series: rows,
          plotOptions: {
            unit: {
              layout: 'scatter',
              unitValue: 1,
              size: 3.5,
              scatter: {
                // Value stays on Y, one lane per plan across X, matching the
                // violins. The value-axis keys keep their x* names in either
                // orientation.
                orientation: 'vertical',
                spread: 'jitter',
                xTitle: 'Minutes per day',
                // The same 0..120 window the violin state pins its yaxis to;
                // 7 ticks puts a line every 20 minutes, matching its grid.
                xMin: 0,
                xMax: 120,
                tickAmount: 7,
              },
            },
          },
          legend: { show: false },
        })
      } else {
        chart.updateOptions({
          chart: { type: 'violin' },
          series: VIOLIN_SERIES,
          legend: { show: false },
        })
      }
    })
  })

  setActive(false)
  renderSummary()
}

var options = {
  series: VIOLIN_SERIES,
  chart: {
    id: 'violinJitter',
    type: 'violin',
    height: 430,
    toolbar: {
      show: false,
    },
    animations: {
      chartTypeMorph: {
        speed: 900,
      },
    },
  },
  colors: COLORS,
  plotOptions: {
    bar: { distributed: true }, // one colour per plan
    violin: {
      normalize: 'group',
      // The toggle is the reveal here; the built-in overlay would spoil it.
      points: { show: false },
    },
  },
  stroke: {
    width: 1,
    colors: ['#8a97a3'],
  },
  legend: {
    show: false,
  },
  yaxis: {
    // Same domain and ticks as the jitter view, so the two states share one
    // grid and the morph never re-scales the room. Minutes cannot be negative,
    // which the auto-domain's padding would otherwise imply.
    min: 0,
    max: 120,
    tickAmount: 6,
    labels: {
      formatter: function (v) {
        return Math.round(v) + ' min'
      },
    },
  },
}

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

// PLANS, VIOLIN_SERIES and wireExplode live in the shared head script.
wireExplode(chart)
Violin to Jitter Morph - JavaScript Violin Charts | ApexCharts.js | ApexCharts.js