Comparing Distributions in JavaScript

Using ApexCharts with JavaScript

This Comparing Distributions example uses ApexCharts.js directly in JavaScript, with no wrapper component.

Install it with npm install apexcharts, then mount the chart with new ApexCharts(element, options).render().

JavaScript installation guide
// Shared by the vanilla-js, React and Vue builds.
//
// Both series carry RAW OBSERVATIONS, one number per trip. The chart bins them,
// and every series is binned against the SAME edges, derived from their combined
// extent. That is what makes two distributions comparable: bin each to its own
// range and identical bars would sit at different values.
//
// 900 door-to-door commute times per mode, from a seeded generator so the page
// is deterministic.
var COMMUTES = (function () {
  var seed = 20260813
  function rand() {
    seed = (seed * 16807) % 2147483647
    return (seed - 1) / 2147483646
  }
  // Box-Muller into a log-normal: journey times are right-skewed, since a trip
  // can go badly wrong but cannot finish in less than no time.
  function trips(n, mu, sigma) {
    var out = []
    for (var i = 0; i < n; i++) {
      var u1 = Math.max(rand(), 1e-9)
      var u2 = rand()
      var z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2)
      out.push(Math.round(Math.exp(mu + z * sigma)))
    }
    return out
  }
  return {
    // Driving is quicker on a typical day and far less predictable: a lower
    // centre, a much heavier tail.
    car: trips(900, 3.25, 0.5),
    transit: trips(900, 3.45, 0.2),
  }
})()

function median(values) {
  var sorted = values.slice().sort(function (a, b) {
    return a - b
  })
  var mid = Math.floor(sorted.length / 2)
  return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2
}

// The bad-day figure: the trip you should actually plan around.
function worstTwentieth(values) {
  var sorted = values.slice().sort(function (a, b) {
    return a - b
  })
  return sorted[Math.floor((sorted.length - 1) * 0.95)]
}

function renderStats() {
  var el = document.querySelector('#stats')
  if (!el) return
  el.innerHTML =
    'Car: typical <b>' +
    median(COMMUTES.car) +
    ' min</b>, ' +
    'bad day <b>' +
    worstTwentieth(COMMUTES.car) +
    ' min</b> &middot; ' +
    'Transit: typical <b>' +
    median(COMMUTES.transit) +
    ' min</b>, ' +
    'bad day <b>' +
    worstTwentieth(COMMUTES.transit) +
    ' min</b>'
}

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

  buttons.forEach(function (b) {
    b.addEventListener('click', function () {
      var overlap = b.getAttribute('data-overlap') === 'true'
      buttons.forEach(function (other) {
        other.className = other === b ? 'on' : ''
      })
      chart.updateOptions({
        plotOptions: { histogram: { overlap: overlap } },
        // The defaults that come with an overlay are ordinary defaults, so a
        // runtime switch has to carry them itself.
        fill: { opacity: overlap ? 0.65 : 0.85 },
        stroke: overlap
          ? { show: false }
          : { show: true, width: 1, colors: ['#fff'] },
      })
    })
  })

  renderStats()
}

var options = {
  series: [
    {
      name: 'Car',
      data: COMMUTES.car,
    },
    {
      name: 'Transit',
      data: COMMUTES.transit,
    },
  ],
  chart: {
    id: 'commutes',
    type: 'histogram',
    // Fixed width, not responsive: thin bars make every bar edge a hairline, so a
    // page-width nudge of a pixel or two visibly moves the whole distribution.
    width: 700,
    height: 400,
    toolbar: {
      show: false,
    },
  },
  plotOptions: {
    histogram: {
      bins: 'auto',
      // The default with more than one series. Every distribution is drawn across
      // the full bin so they lie on top of one another; set false for side-by-side
      // bars. All series share one set of bin edges either way.
      overlap: true,
    },
  },
  colors: ['#f2a43a', '#5d6d9e'],
  xaxis: {
    title: {
      text: 'Door-to-door time (minutes)',
    },
    labels: {
      formatter: function (val) {
        return Math.round(val)
      },
    },
  },
  yaxis: {
    title: {
      text: 'Trips',
    },
  },
  legend: {
    position: 'top',
    horizontalAlign: 'right',
  },
}

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

// COMMUTES and wireComparisonControls live in the shared head script.
wireComparisonControls(chart)