Heatmap with Drilldown in JavaScript

Using ApexCharts with JavaScript

This Heatmap with Drilldown 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
// Root grid: regions (rows) x quarters (columns). Every cell carries a drilldown
// id, so clicking any cell opens that region+quarter broken down into a
// months (rows) x weeks (columns) heatmap. Values are deterministic so a
// re-drill always shows the same detail.
var regions = ['North America', 'Europe', 'Asia Pacific', 'Latin America']
var quarters = ['Q1', 'Q2', 'Q3', 'Q4']
var monthsByQuarter = [
  ['Jan', 'Feb', 'Mar'],
  ['Apr', 'May', 'Jun'],
  ['Jul', 'Aug', 'Sep'],
  ['Oct', 'Nov', 'Dec'],
]
var weeks = ['Wk 1', 'Wk 2', 'Wk 3', 'Wk 4']

function pseudo(seed, lo, hi) {
  var x = Math.sin(seed) * 10000
  x = x - Math.floor(x)
  return Math.round(lo + x * (hi - lo))
}

function clamp(v, lo, hi) {
  return Math.max(lo, Math.min(hi, v))
}

// One shared 0-100 colour scale for every level. Sales intensity is a score, so
// the Low/Medium/High/Peak thresholds mean the same thing whether you are
// looking at a region+quarter or at one of its weeks. That is what lets the
// drilldown stay honest: a "Peak" quarter drills into weeks that are mostly
// Peak/High too, not a fresh unrelated set of numbers.
var heatPlot = {
  heatmap: {
    radius: 2,
    shadeIntensity: 0.5,
    colorScale: {
      ranges: [
        { from: 0, to: 30, color: '#C8E6C9', name: 'Low' },
        { from: 31, to: 60, color: '#66BB6A', name: 'Medium' },
        { from: 61, to: 80, color: '#FB8C00', name: 'High' },
        { from: 81, to: 100, color: '#E53935', name: 'Peak' },
      ],
    },
  },
}

// The region+quarter score is the headline for its 12 weekly cells (3 months x
// 4 weeks), so compute it once and reuse it as the anchor for the detail.
var rootValues = regions.map(function (region, ri) {
  return quarters.map(function (q, qi) {
    return pseudo(ri * 31 + qi * 7 + 3, 20, 95)
  })
})

var heatRootSeries = regions.map(function (region, ri) {
  return {
    name: region,
    data: quarters.map(function (q, qi) {
      return { x: q, y: rootValues[ri][qi], drilldown: 'r' + ri + '-q' + qi }
    }),
  }
})

var heatChildren = []
regions.forEach(function (region, ri) {
  quarters.forEach(function (q, qi) {
    var base = rootValues[ri][qi]
    var months = monthsByQuarter[qi]

    // Draw a raw +/-15 wobble for each of the 12 weekly cells, then re-center
    // the wobbles so they sum to zero. The weekly cells therefore average back
    // to the region+quarter score: the drilldown preserves the headline instead
    // of inventing a new total, while individual weeks still vary realistically.
    var raw = []
    months.forEach(function (m, mi) {
      weeks.forEach(function (wk, wi) {
        raw.push(pseudo(ri * 101 + qi * 53 + mi * 17 + wi * 3 + 11, -15, 15))
      })
    })
    var mean =
      raw.reduce(function (a, b) {
        return a + b
      }, 0) / raw.length
    var k = 0

    heatChildren.push({
      id: 'r' + ri + '-q' + qi,
      name: region + ' / ' + q,
      series: months.map(function (m, mi) {
        return {
          name: m,
          data: weeks.map(function (wk, wi) {
            var y = clamp(Math.round(base + raw[k] - mean), 0, 100)
            k++
            return { x: wk, y: y }
          }),
        }
      }),
    })
  })
})

var options = {
  series: heatRootSeries,
  chart: {
    type: 'heatmap',
    height: 450,
    toolbar: {
      show: false,
    },
  },
  dataLabels: {
    enabled: true,
  },
  plotOptions: heatPlot,
  title: {
    text: 'Sales Intensity by Region and Quarter',
    align: 'left',
  },
  subtitle: {
    text: 'Click any cell to drill into its weekly breakdown (pointer cursor marks drillable cells). Use the breadcrumb to go back.',
    align: 'left',
  },
  drilldown: {
    enabled: true,
    breadcrumb: {
      show: true,
      position: 'top-right',
      rootLabel: 'All Regions',
      separator: ' / ',
    },
    series: heatChildren,
  },
}

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