Canvas Renderer (High-Density) in Vue

Using ApexCharts with Vue

This Canvas Renderer (High-Density) example wires ApexCharts into Vue through the vue3-apexcharts component.

Install it with npm install vue3-apexcharts apexcharts, then mount the chart with <apexchart type="..." :options="options" :series="series" />.

Vue charts guide
<template>
  <div>
    <div class="panel">
      <div class="controls">
        <label>
          Renderer:
          <select id="rendererMode">
            <option value="auto" selected>auto</option>
            <option value="svg">svg</option>
            <option value="canvas">canvas</option>
          </select>
        </label>
        <label>
          Points:
          <select id="pointCount">
            <option value="2000">2,000</option>
            <option value="10000">10,000</option>
            <option value="50000" selected>50,000</option>
            <option value="100000">100,000</option>
          </select>
        </label>
        <button id="rerender">Re-render</button>
        <button id="resetZoom">Reset zoom</button>
      </div>

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

      <div class="note">
        The series markers are painted into a single
        <code>&lt;canvas&gt;</code> while the axes, grid, zoom selection,
        legend, and shared tooltip stay as SVG. With <b>renderer: 'auto'</b> the
        canvas layer switches on once the rendered-mark count crosses
        <code>rendererThreshold</code> (8,000), so 2,000 points draw as SVG and
        10,000+ promote to canvas. Switch the Renderer control to <b>svg</b> at
        50,000+ points to feel the difference: the DOM-node count jumps from a
        few dozen to one node per marker.
      </div>
    </div>

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

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

import 'apexcharts/features/renderer-canvas'

var pointCount = 50000

// Box-Muller gaussian: gives two legible clusters at any density instead of
// uniform noise, so the plotted shape stays readable whether it is 2,000 or
// 100,000 points.
function gauss(mean, sd) {
  var u = 1 - Math.random()
  var v = Math.random()
  return mean + sd * Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v)
}

function generateClusters(count) {
  var half = Math.floor(count / 2)
  var a = new Array(half)
  var b = new Array(count - half)
  for (var i = 0; i < a.length; i++) {
    a[i] = [+gauss(32, 9).toFixed(2), +gauss(62, 15).toFixed(2)]
  }
  for (var j = 0; j < b.length; j++) {
    b[j] = [+gauss(70, 10).toFixed(2), +gauss(34, 13).toFixed(2)]
  }
  return [a, b]
}

var clusters = generateClusters(pointCount)
var lastRenderMs = 0

// Every SVG mark is a live DOM node, so counting nodes inside the chart makes
// the hybrid renderer visible: canvas keeps this in the dozens no matter how
// many points are plotted.
function domNodeCount() {
  var el = document.querySelector('#chart')
  return el ? el.getElementsByTagName('*').length : 0
}

// Accepts the live chart instance as an argument so the React and Vue builds
// (which have no global `chart` var) can share this stateless helper.
function updateStats(chart) {
  var el = document.getElementById('stats')
  if (!el || !chart) return
  var active = chart.getActiveRenderer ? chart.getActiveRenderer() : 'svg'
  el.innerHTML =
    'Points: <b>' +
    pointCount.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>'
}

export default {
  components: {
    apexchart: VueApexCharts,
  },
  data: function () {
    return {
      series: [
        { name: 'Cluster A', data: clusters[0] },
        { name: 'Cluster B', data: clusters[1] },
      ],
      chartOptions: {
        chart: {
          height: 480,
          type: 'scatter',
          renderer: 'auto',
          rendererThreshold: 8000,
          animations: { enabled: false },
          zoom: { enabled: true, type: 'xy' },
          toolbar: { autoSelected: 'zoom' },
        },
        markers: { size: 3, strokeWidth: 0 },
        colors: ['#008FFB', '#FF4560'],
        title: {
          text: 'Scatter: 50,000 points on the auto-selected renderer',
          align: 'left',
        },
        subtitle: {
          text: 'Dense series marks paint to a canvas layer; axes, grid, zoom and tooltip stay SVG',
          align: 'left',
        },
        xaxis: {
          type: 'numeric',
          tickAmount: 10,
          labels: {
            formatter: function (val) {
              return parseFloat(val).toFixed(0)
            },
          },
        },
        yaxis: {
          tickAmount: 8,
          labels: {
            formatter: function (val) {
              return val.toFixed(0)
            },
          },
        },
        legend: { position: 'top' },
        tooltip: { shared: true, intersect: false },
      },
      scatterChart: null,
      scatterDisposed: false,
    }
  },
  mounted: function () {
    // This demo destroys and recreates the chart imperatively on every re-render,
    // so it skips the vue-apexcharts wrapper and creates the instance in #chart
    // directly, exactly like the vanilla build. pointCount, clusters,
    // generateClusters, domNodeCount and updateStats live in the shared head
    // script.
    var me = this
    var chart = null

    // The vanilla build gets a mutable `options` global from the template; rebuild
    // it here from the parsed chart state.
    var options = Object.assign({ series: me.series }, me.chartOptions)
    options.chart = Object.assign({}, me.chartOptions.chart)
    options.title = Object.assign({}, me.chartOptions.title)

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

      clusters = generateClusters(pointCount)
      options.chart.renderer = mode
      options.series = [
        { name: 'Cluster A', data: clusters[0] },
        { name: 'Cluster B', data: clusters[1] },
      ]
      options.title.text =
        'Scatter: ' +
        pointCount.toLocaleString() +
        ' points on the ' +
        (mode === 'auto' ? 'auto-selected' : mode) +
        ' renderer'

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

    document.getElementById('rerender').addEventListener('click', rebuild)
    document.getElementById('rendererMode').addEventListener('change', rebuild)
    document.getElementById('pointCount').addEventListener('change', rebuild)
    document.getElementById('resetZoom').addEventListener('click', function () {
      if (chart) chart.resetSeries(true, true)
    })

    // Initial render into #chart, then fill in the live metrics.
    chart = new ApexCharts(document.querySelector('#chart'), options)
    me.scatterChart = chart
    chart.render().then(function () {
      if (me.scatterDisposed) return
      updateStats(chart)
    })
  },
  beforeDestroy: function () {
    this.scatterDisposed = true
    if (this.scatterChart) this.scatterChart.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: #f4f8ff;
  border-left: 3px solid #5b8cff;
  padding: 10px 14px;
  font-size: 13px;
  color: #234;
  margin-top: 10px;
  border-radius: 2px;
}
.note code {
  background: #e6eeff;
  padding: 1px 5px;
  border-radius: 3px;
}
</style>