Large Dataset Downsampling in Vue

Using ApexCharts with Vue

This Large Dataset Downsampling 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>
          <input type="checkbox" id="toggleReducer" checked />
          Enable LTTB downsampling
        </label>
        <label>
          Target points:
          <input
            type="number"
            id="targetPoints"
            class="target-input"
            value="500"
            min="50"
            max="5000"
            step="50"
          />
        </label>
        <button id="rerender">Re-render</button>
        <button id="resetZoom">Reset zoom</button>
      </div>

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

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

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

var TOTAL_POINTS = 500000

function generateData(count) {
  var data = new Array(count)
  var startTime = new Date('2010-01-01T00:00:00Z').getTime()
  var step = 60 * 1000 // 1 minute
  var y = 100
  for (var i = 0; i < count; i++) {
    // Random walk with periodic components — gives visible structure at
    // multiple zoom levels (slow trend + faster cycle + noise).
    y +=
      (Math.random() - 0.5) * 2 +
      Math.sin(i / 5000) * 0.05 +
      Math.cos(i / 1200) * 0.02
    data[i] = [startTime + i * step, +y.toFixed(3)]
  }
  return data
}

var seriesData = generateData(TOTAL_POINTS)
var lastRenderMs = 0
var lastZoomLabel = 'full range'

function fmt(ts) {
  var d = new Date(ts)
  return (
    d.getFullYear() +
    '-' +
    String(d.getMonth() + 1).padStart(2, '0') +
    '-' +
    String(d.getDate()).padStart(2, '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 statsEl = document.getElementById('stats')
  if (!statsEl || !chart || !chart.w) return
  var drawn = (chart.w.globals.series[0] || []).length
  statsEl.innerHTML =
    'Raw points: <b>' +
    TOTAL_POINTS.toLocaleString() +
    '</b> &nbsp;·&nbsp; Currently drawn: <b>' +
    drawn.toLocaleString() +
    '</b> &nbsp;·&nbsp; Window: <b>' +
    lastZoomLabel +
    '</b> &nbsp;·&nbsp; Render: <b>' +
    lastRenderMs +
    ' ms</b>'
}

export default {
  components: {
    apexchart: VueApexCharts,
  },
  data: function () {
    return {
      series: [{ name: 'Sensor reading', data: seriesData }],
      chartOptions: {
        chart: {
          height: 460,
          type: 'line',
          animations: { enabled: false },
          zoom: { enabled: true, type: 'x', autoScaleYaxis: true },
          toolbar: { autoSelected: 'zoom' },
          dataReducer: {
            enabled: true,
            algorithm: 'lttb',
            targetPoints: 500,
            threshold: 1000,
          },
          events: {
            zoomed: function (chartCtx, args) {
              var xaxis = args && args.xaxis
              lastZoomLabel =
                xaxis && xaxis.min && xaxis.max
                  ? fmt(xaxis.min) + ' → ' + fmt(xaxis.max)
                  : 'full range'
              requestAnimationFrame(function () {
                updateStats(chartCtx)
              })
            },
            beforeResetZoom: function () {
              lastZoomLabel = 'full range'
            },
          },
        },
        stroke: { curve: 'straight', width: 1 },
        dataLabels: { enabled: false },
        markers: { size: 0 },
        title: {
          text: 'Line chart — 500,000 raw datapoints',
          align: 'left',
        },
        subtitle: {
          text: 'LTTB downsampling enabled — zoom in to re-sample the raw series at higher resolution within the visible window',
          align: 'left',
        },
        xaxis: {
          type: 'datetime',
          labels: { datetimeUTC: false },
        },
        yaxis: {
          labels: {
            formatter: function (val) {
              return val.toFixed(2)
            },
          },
        },
        tooltip: { x: { format: 'yyyy-MM-dd HH:mm' } },
      },
      reducerChart: null,
      reducerDisposed: 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. TOTAL_POINTS, seriesData, fmt,
    // updateStats and the lastRenderMs / lastZoomLabel state 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.chart.dataReducer = Object.assign(
      {},
      me.chartOptions.chart.dataReducer,
    )

    function rebuild() {
      var enabled = document.getElementById('toggleReducer').checked
      var targetPoints = parseInt(
        document.getElementById('targetPoints').value,
        10,
      )

      options.chart.dataReducer.enabled = enabled
      options.chart.dataReducer.targetPoints = targetPoints
      options.subtitle.text = enabled
        ? 'LTTB downsampling enabled — drawing ~' +
          targetPoints.toLocaleString() +
          ' points'
        : 'Downsampling disabled — drawing every raw point'

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

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

    // Initial render into #chart, then seed the stats once the first paint settles.
    chart = new ApexCharts(document.querySelector('#chart'), options)
    me.reducerChart = chart
    chart.render().then(function () {
      if (me.reducerDisposed) return
      updateStats(chart)
    })
  },
  beforeDestroy: function () {
    this.reducerDisposed = true
    if (this.reducerChart) this.reducerChart.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 button {
  padding: 6px 14px;
  cursor: pointer;
}
.target-input {
  width: 80px;
}
.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;
}
</style>