Top and Bottom Markets in JavaScript

Using ApexCharts with JavaScript

This Top and Bottom Markets 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
// Forty-two markets, three years of monthly activation rate, no axes at all.
// At this size an axis costs more than it pays: the shared scale is stated
// once in the caption, each panel labels its own latest value, and the ink
// left over is the trend. The five best and five weakest latest rates are
// tinted so the extremes are findable without reading 42 numbers.
//
// Deterministic series so the e2e snapshot is stable.
function mulberry32(seed) {
  return function () {
    seed |= 0
    seed = (seed + 0x6d2b79f5) | 0
    var t = Math.imul(seed ^ (seed >>> 15), 1 | seed)
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296
  }
}

var MARKETS = [
  'Amsterdam',
  'Antwerp',
  'Athens',
  'Auckland',
  'Austin',
  'Bergen',
  'Bilbao',
  'Bologna',
  'Bristol',
  'Calgary',
  'Cardiff',
  'Cork',
  'Dublin',
  'Dundee',
  'Edinburgh',
  'Galway',
  'Ghent',
  'Graz',
  'Helsinki',
  'Kingston',
  'Leeds',
  'Leiden',
  'Lisbon',
  'Lyon',
  'Malaga',
  'Malmo',
  'Nantes',
  'Oporto',
  'Oslo',
  'Ottawa',
  'Padua',
  'Perth',
  'Reykjavik',
  'Rotterdam',
  'Salerno',
  'Seville',
  'Tampere',
  'Turin',
  'Turku',
  'Utrecht',
  'Verona',
  'Zurich',
]

var MONTHS = 36
var START = new Date(2023, 0, 1).getTime()
var MONTH_MS = 30.44 * 24 * 3600 * 1000

// A trend plus a wobble, so the panels have shape rather than noise.
function activationSeries(seed, start, drift) {
  var rand = mulberry32(seed)
  var out = []
  var v = start
  for (var m = 0; m < MONTHS; m++) {
    v += drift + (rand() - 0.5) * 0.55
    // A mild seasonal lift in the second half of each year.
    var season = Math.sin(((m % 12) / 12) * Math.PI * 2) * 0.28
    out.push({
      x: Math.round(START + m * MONTH_MS),
      y: Math.max(1.2, Math.round((v + season) * 100) / 100),
    })
  }
  return out
}

var marketSeries = MARKETS.map(function (name, i) {
  var rand = mulberry32(1000 + i * 13)
  var start = 2.4 + rand() * 3.2
  var drift = (rand() - 0.42) * 0.11
  return {
    name: 'Activation rate',
    market: name,
    data: activationSeries(200 + i * 7, start, drift),
  }
})

// Rank on the LATEST value: the five best get the teal tint, the five
// weakest the amber one, everything else stays quiet grey.
var latest = {}
marketSeries.forEach(function (s) {
  latest[s.market] = s.data[s.data.length - 1].y
})
var ranked = MARKETS.slice().sort(function (a, b) {
  return latest[b] - latest[a]
})
var TOP = {}
var BOTTOM = {}
ranked.slice(0, 5).forEach(function (m) {
  TOP[m] = true
})
ranked.slice(-5).forEach(function (m) {
  BOTTOM[m] = true
})

var options = {
  series: marketSeries,
  chart: {
    id: 'marketTrellis',
    type: 'line',
    height: 700,
    animations: {
      enabled: false,
    },
  },
  trellis: {
    by: 'market',
    columns: 7,
    minPanelWidth: 130,
    panelHeight: 76,
    gap: 6,
    header: {
      style: {
        fontSize: '11px',
        fontWeight: 600,
      },
    },
    panel: function (key) {
      // The value label rides the last point, so which SIDE of it has room
      // depends on where that point sits in the shared 0-10 frame: below for a
      // high panel, above for a low one. `panel()` is merged last, so the
      // per-panel offset is a two-line override.
      var side = { dataLabels: { offsetY: latest[key] > 5 ? 15 : -8 } }
      if (TOP[key]) {
        return Object.assign(
          { colors: ['#0F766E'], chart: { background: '#EAF4F2' } },
          side,
        )
      }
      if (BOTTOM[key]) {
        return Object.assign(
          { colors: ['#B45309'], chart: { background: '#FBF1E3' } },
          side,
        )
      }
      return Object.assign({ colors: ['#94A3B8'] }, side)
    },
  },
  stroke: {
    width: 1.5,
    curve: 'straight',
  },
  markers: {
    size: 0,
    discrete: [
      {
        seriesIndex: 0,
        dataPointIndex: 35,
        size: 3,
        strokeWidth: 0,
      },
    ],
  },
  dataLabels: {
    enabled: true,
    offsetX: -10,
    offsetY: -8,
    background: {
      enabled: false,
    },
    style: {
      fontSize: '10px',
      fontWeight: 600,
      colors: ['#475569'],
    },
    formatter: function (val, opts) {
      var d = opts.w.config.series[opts.seriesIndex].data
      return opts.dataPointIndex === d.length - 1 ? Number(val).toFixed(1) : ''
    },
  },
  yaxis: {
    labels: {
      show: false,
    },
  },
  xaxis: {
    type: 'datetime',
    labels: {
      show: false,
    },
    axisTicks: {
      show: false,
    },
    axisBorder: {
      show: false,
    },
    tooltip: {
      enabled: false,
    },
  },
  grid: {
    show: false,
    padding: {
      left: 4,
      right: 4,
    },
  },
  tooltip: {
    x: {
      format: 'MMM yyyy',
    },
  },
}

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