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

// 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()
}

const ApexChart = () => {
  const [state, setState] = React.useState({
    series: VIOLIN_SERIES,
    options: {
      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'
          },
        },
      },
    },
  })

  React.useEffect(() => {
    // The react-apexcharts wrapper owns the render, so reach the live instance
    // by its chart.id before wiring the controls.
    const timer = window.setInterval(() => {
      const chart = ApexCharts.getChartByID('violinJitter')
      if (!chart) return
      window.clearInterval(timer)
      wireExplode(chart)
    }, 50)

    return () => window.clearInterval(timer)
  }, [])

  return (
    <div>
      <div className="wrap">
        <h1>A violin is an estimate</h1>
        <p>
          Each curve below is a density estimate: a smoothed guess at where the
          readings sit, computed from the readings themselves. Smoothing is the
          point, and also the catch. A curve cannot show how many readings it
          stands on, and it rounds hard edges off. Press the button and each
          violin dissolves into its actual readings, jittered across the lane,
          then gathers back into the curve.
        </p>

        <div className="actions">
          <button data-explode="false" className="on">
            Violin
          </button>
          <button data-explode="true">Every reading</button>
        </div>

        <div className="chart-wrap">
          <div id="chart">
            <ReactApexChart
              options={state.options}
              series={state.series}
              type="violin"
              height={430}
            />
          </div>
        </div>

        <div className="summary" id="summary"></div>

        <div className="note">
          Free is capped at 30 minutes a day, and the wall of readings pinned at
          exactly 30 comes out of the estimate as a gentle bulge that even
          glides a little past the cap, where no reading exists at all. Trial's
          curve looks as confident as the others; it stands on 14 readings. The
          violins are derived, not supplied: each datum carries raw{' '}
          <code>points</code> and the library runs the density estimate. That is
          also what makes the dissolve possible, because{' '}
          <code>chart.rowSeries()</code> hands back the observations behind
          every mark; the jitter view is the unit type's scatter layout with
          <code>spread: 'jitter'</code>. To see dots and curve at once without
          morphing, a violin can overlay its own via
          <code>plotOptions.violin.points</code>.
        </div>
      </div>
    </div>
  )
}

export default ApexChart
Violin to Jitter Morph - React Violin Charts | ApexCharts.js | ApexCharts.js