Horizontal Bimodal in JavaScript

Using ApexCharts with JavaScript

This Horizontal Bimodal 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
// ── Reproducible synthetic data (seeded PRNG, no network) ──────────────
function mulberry32(a) {
  return function () {
    a |= 0
    a = (a + 0x6d2b79f5) | 0
    var t = Math.imul(a ^ (a >>> 15), 1 | a)
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296
  }
}
var rng = mulberry32(24681012)

function gauss(mean, sd) {
  var u = 0,
    v = 0
  while (u === 0) u = rng()
  while (v === 0) v = rng()
  return mean + sd * Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v)
}

// Draw n samples from a mixture of two well-separated normal components — the
// two clusters give each distribution its characteristic twin-hump shape.
function bimodal(n, a, b) {
  var out = []
  for (var i = 0; i < n; i++) {
    var c = rng() < a.w ? a : b
    out.push(Math.max(0, Math.round(gauss(c.mean, c.sd) * 10) / 10))
  }
  return out
}

// Session duration (minutes) per channel — a quick-bounce cluster and an
// engaged-visit cluster, mixed in different proportions.
var CHANNELS = [
  {
    name: 'Direct',
    data: bimodal(320, { w: 0.4, mean: 2, sd: 1 }, { mean: 22, sd: 5 }),
  },
  {
    name: 'Search',
    data: bimodal(320, { w: 0.55, mean: 2, sd: 1 }, { mean: 18, sd: 5 }),
  },
  {
    name: 'Social',
    data: bimodal(320, { w: 0.7, mean: 2, sd: 1 }, { mean: 14, sd: 4 }),
  },
  {
    name: 'Email',
    data: bimodal(320, { w: 0.3, mean: 3, sd: 1.5 }, { mean: 26, sd: 6 }),
  },
]

// Gaussian KDE with a loose bandwidth + coarse step, so the two humps stay
// distinct (a tighter bandwidth would smear them into one) and the outline
// keeps its angular, twin-peaked character.
function kde(values, bw, step) {
  var min = Math.min.apply(null, values) - bw
  var max = Math.max.apply(null, values) + bw
  var N = values.length
  var profile = []
  for (var x = min; x <= max; x += step) {
    var sum = 0
    for (var k = 0; k < N; k++) {
      var d = (x - values[k]) / bw
      sum += Math.exp(-0.5 * d * d)
    }
    profile.push([Math.round(x * 100) / 100, sum / (N * bw)])
  }
  return profile
}

var options = {
  series: [
    {
      name: 'Sessions',
      data: CHANNELS.map(function (c) {
        return {
          x: c.name,
          y: { density: kde(c.data, 2.5, 1.5), points: c.data },
        }
      }),
    },
  ],
  chart: {
    type: 'violin',
    height: 480,
  },
  title: {
    text: 'Session duration by channel',
  },
  plotOptions: {
    bar: {
      horizontal: true,
      distributed: true,
    },
    violin: {
      normalize: 'group',
      points: { show: false }, // density curves only
    },
  },
  colors: ['#0ea5e9', '#F43F5E', '#f97316', '#10b981'],
  stroke: { width: 1, colors: ['#888'] },
  legend: { show: false },
  xaxis: {
    title: { text: 'Session duration (minutes)' },
  },
}

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