<template>
  <div>
    <div class="panel">
      <div class="controls">
        <label>
          Renderer:
          <select id="rendererMode">
            <option value="canvas" selected>canvas</option>
            <option value="svg">svg</option>
            <option value="auto">auto</option>
          </select>
        </label>
        <label>
          Cells:
          <select id="cellCount">
            <option value="2500">2,500</option>
            <option value="10000" selected>10,000</option>
            <option value="50000">50,000</option>
            <option value="100000">100,000</option>
          </select>
        </label>
        <button id="rerender">Re-render</button>
      </div>

      <div class="stats" id="stats"></div>

      <div class="note">
        With the <b>SVG</b> renderer a heatmap draws one
        <code>&lt;rect&gt;</code> per cell, so the DOM node count grows to
        roughly one node per cell. Switch the <b>Renderer</b> to
        <b>canvas</b> and the cells paint to a single canvas instead: the node
        count stays flat and the render time drops sharply at high cell counts.
        Hover still shows a tooltip in both modes. Use the controls to scale the
        cell count and compare.
      </div>
    </div>

    <div id="chart"></div>
  </div>
</template>

<script>
import VueApexCharts from 'vue-apexcharts'
import ApexCharts from 'apexcharts'

var cellCount = 10000
var DAY = 86400000

// A heatmap is a rows x cols matrix; each cell is one value. The cell count
// is what scales cost, so map a target cell count to a rows/cols shape. Here
// the rows are latitude bands (north to south) and the columns are days, so
// the matrix is a daily mean-temperature field: warm near the equator, cold
// toward the poles, with a seasonal wave that grows toward the poles and runs
// half a year out of phase between the two hemispheres.
function shapeFor(cells) {
  if (cells <= 10000) return { rows: 50, cols: cells / 50 }
  return { rows: 100, cols: cells / 100 }
}

function formatLat(lat) {
  var r = Math.round(lat)
  if (r > 0) return r + '°N'
  if (r < 0) return -r + '°S'
  return '0°'
}

function generateHeatmap(cells) {
  var shape = shapeFor(cells)
  // Columns are consecutive days ending today, anchored to UTC midnight so
  // every day is exactly 86400000 ms and the seasonal phase never drifts.
  var now = new Date()
  var end = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
  var start = end - (shape.cols - 1) * DAY

  // Deterministic PRNG so the field looks the same on every reload.
  var seed = 1013904223
  function rand() {
    seed = (seed * 1103515245 + 12345) & 0x7fffffff
    return seed / 0x7fffffff
  }

  var series = new Array(shape.rows)
  for (var r = 0; r < shape.rows; r++) {
    var lat = 90 - (r / (shape.rows - 1)) * 180
    var absLat = Math.abs(lat) / 90
    // Warm at the equator (~27C), cold at the poles (~-30C).
    var baseTemp = 27 - 57 * Math.pow(absLat, 1.05)
    // Seasonal swing: tiny at the equator, large toward the poles.
    var swing = 2 + 22 * absLat
    var hemi = lat >= 0 ? 1 : -1
    var data = new Array(shape.cols)
    for (var c = 0; c < shape.cols; c++) {
      var t = start + c * DAY
      var d = new Date(t)
      var doy = (t - Date.UTC(d.getUTCFullYear(), 0, 0)) / DAY
      // Northern hemisphere peaks in mid-July; southern is the opposite.
      var seasonal = swing * Math.sin((2 * Math.PI * doy) / 365 - 1.87) * hemi
      var temp = baseTemp + seasonal + (rand() - 0.5) * 3
      data[c] = { x: t, y: Math.round(temp) }
    }
    series[r] = { name: formatLat(lat), data: data }
  }
  // Rows read north (top) -> south (bottom); ApexCharts draws the last series
  // on top, so reverse before returning.
  return series.reverse()
}

var heatmapData = generateHeatmap(cellCount)

// Stateless helper shared by the vanilla, React and Vue builds.
function domNodeCount() {
  var el = document.querySelector('#chart')
  return el ? el.getElementsByTagName('*').length : 0
}

export default {
components: {
apexchart: VueApexCharts,
},
data: function () {
return {
series: heatmapData,
chartOptions: {
chart: {
  height: 500,
  type: 'heatmap',
  renderer: 'canvas',
  rendererThreshold: 8000,
  animations: { enabled: false },
  toolbar: { show: false },
},
title: {
  text: 'Heatmap: 10,000 cells',
  align: 'left',
},
subtitle: {
  text: 'Daily mean surface temperature (°C) by latitude, cold (blue) to warm (red)',
  align: 'left',
},
dataLabels: { enabled: false },
// No cell stroke: at high cell counts each cell is ~1px, and a stroke that
// wide would cover the fill entirely (cells look blank). Solid fills only.
stroke: { show: false },
plotOptions: {
  heatmap: {
    // Flat temperature buckets (no within-bucket shading) on a cold-to-warm
    // scale, so a cell's color reads as a temperature straight off the legend.
    enableShades: false,
    colorScale: {
      ranges: [
        { from: -60, to: -25, color: '#313695', name: 'below -25' },
        { from: -25, to: -15, color: '#4575b4', name: '-25 to -15' },
        { from: -15, to: -5, color: '#74add1', name: '-15 to -5' },
        { from: -5, to: 2, color: '#abd9e9', name: '-5 to 2' },
        { from: 2, to: 9, color: '#fee090', name: '2 to 9' },
        { from: 9, to: 16, color: '#fdae61', name: '9 to 16' },
        { from: 16, to: 23, color: '#f46d43', name: '16 to 23' },
        { from: 23, to: 60, color: '#d73027', name: 'above 23' },
      ],
    },
  },
},
xaxis: {
  type: 'datetime',
  labels: { datetimeUTC: true, style: { fontSize: '12px' } },
  axisTicks: { show: false },
  axisBorder: { show: false },
},
yaxis: {
  labels: { style: { fontSize: '12px' } },
},
},
heatmapChart: null,
heatmapDisposed: false,
}
},
mounted: function () {
  // This demo destroys and recreates the chart on every control change, so it
  // skips the vue-apexcharts wrapper and creates the instance in #chart
  // directly, like the vanilla build. cellCount, heatmapData, generateHeatmap,
  // shapeFor and domNodeCount live in the shared head script.
  var me = this
  var lastRenderMs = 0
  var chart = null

  var options = Object.assign({ series: me.series }, me.chartOptions)
  options.chart = Object.assign({}, me.chartOptions.chart)
  options.title = Object.assign({}, me.chartOptions.title)

  function updateStats() {
    var el = document.getElementById('stats')
    if (!el || !chart) return
    var active = chart.getActiveRenderer ? chart.getActiveRenderer() : 'svg'
    el.innerHTML =
      'Cells: <b>' +
      cellCount.toLocaleString() +
      '</b> &nbsp;·&nbsp; Active renderer: <b>' +
      active.toUpperCase() +
      '</b> &nbsp;·&nbsp; DOM nodes in chart: <b>' +
      domNodeCount().toLocaleString() +
      '</b> &nbsp;·&nbsp; Render: <b>' +
      (lastRenderMs > 0 ? lastRenderMs + ' ms' : 'change a control to time it') +
      '</b>'
  }

  function rebuild() {
    cellCount = parseInt(document.getElementById('cellCount').value, 10)
    var mode = document.getElementById('rendererMode').value

    heatmapData = generateHeatmap(cellCount)
    options.chart.renderer = mode
    options.series = heatmapData
    options.title.text = 'Heatmap: ' + cellCount.toLocaleString() + ' cells'

    var t = performance.now()
    if (chart) chart.destroy()
    chart = new ApexCharts(document.querySelector('#chart'), options)
    me.heatmapChart = chart
    chart.render().then(function () {
      if (me.heatmapDisposed) return
      lastRenderMs = Math.round(performance.now() - t)
      updateStats()
    })
  }

  document.getElementById('rerender').addEventListener('click', rebuild)
  document.getElementById('rendererMode').addEventListener('change', rebuild)
  document.getElementById('cellCount').addEventListener('change', rebuild)

  chart = new ApexCharts(document.querySelector('#chart'), options)
  me.heatmapChart = chart
  chart.render().then(function () {
    if (me.heatmapDisposed) return
    updateStats()
  })
},
beforeDestroy: function () {
  this.heatmapDisposed = true
  if (this.heatmapChart) this.heatmapChart.destroy()
},,
}
</script>

<style>
#chart {
  max-width: 1000px;
  margin: 20px auto;
}
.panel {
  max-width: 1000px;
  margin: 16px auto;
  font-family: sans-serif;
}
.controls {
  display: flex;
  gap: 14px;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 8px;
}
.controls label {
  font-size: 14px;
}
.controls select {
  padding: 4px 6px;
  font-size: 14px;
}
.controls button {
  padding: 6px 14px;
  cursor: pointer;
}
.stats {
  font-family: monospace;
  font-size: 13px;
  color: #555;
  line-height: 1.6;
}
.stats b {
  color: #111;
}
.note {
  background: #fff7f0;
  border-left: 3px solid #ff9f5b;
  padding: 10px 14px;
  font-size: 13px;
  color: #432;
  margin-top: 10px;
  border-radius: 2px;
}
.note code {
  background: #ffe9d6;
  padding: 1px 5px;
  border-radius: 3px;
}
</style>
Canvas Renderer (High-Density) - Vue Heatmap Charts | ApexCharts.js | ApexCharts.js