Undo / Redo (Rewind) in Vue

Using ApexCharts with Vue

This Undo / Redo (Rewind) 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">
        <button id="undo" class="primary" disabled>Undo</button>
        <button id="redo" class="primary" disabled>Redo</button>
        <button id="randomize">Randomize data</button>
        <button id="reset">Clear history</button>
        <span class="readout" id="readout">history: empty</span>
      </div>
    </div>

    <div id="chart">
      <apexchart
        type="line"
        height="400"
        :options="chartOptions"
        :series="series"
      ></apexchart>
    </div>

    <div class="panel">
      <div class="note">
        History is enabled with
        <code>chart: &#123; history: &#123; enabled: true &#125; &#125;</code>.
        Every data update, zoom, pan, annotation and measurement commits a
        checkpoint. <b>Right-click</b> the plot to add a note, drop a dashed
        marker line, or start a measurement (context menu);
        <b>drag-select</b> or use the toolbar to zoom and pan; hold
        <code>m</code> and drag to measure; press Randomize to replace the data.
        Then step back with Undo / Redo (or press <code>Cmd/Ctrl + Z</code> and
        <code>Shift + Cmd/Ctrl + Z</code>). The buttons enable from the
        <code>historyChange</code> event via <code>chart.history.state()</code>.
      </div>
    </div>
  </div>
</template>

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

import 'apexcharts/features/history'
import 'apexcharts/features/context-menu'
import 'apexcharts/features/ink'
import 'apexcharts/features/measure'

// A wandering line (numeric x, ~45 points) so there is room to zoom, pan,
// annotate and measure. Randomize regenerates it into a fresh checkpoint.
function randomSeries() {
  var d = []
  var v = 45 + Math.random() * 25
  for (var i = 0; i < 45; i++) {
    v += (Math.random() - 0.5) * 22
    if (v < 8) v = 8
    if (v > 100) v = 100
    d.push([i + 1, Math.round(v)])
  }
  return d
}

export default {
  components: {
    apexchart: VueApexCharts,
  },
  data: function () {
    return {
      series: [{ name: 'Value', data: randomSeries() }],
      chartOptions: {
        chart: {
          height: 400,
          type: 'line',
          id: 'rewind-demo',
          fontFamily: 'Helvetica, Arial, sans-serif',
          animations: { enabled: true },
          // Toolbar on so zoom / pan / reset (and the measure tool) are one click away.
          toolbar: { show: true, autoSelected: 'zoom' },
          zoom: { enabled: true, type: 'x' },
          history: {
            enabled: true,
            maxDepth: 50,
            coalesceMs: 250,
            keyboard: true,
          },
          // Right-click actions: drop a note, a dashed marker line, or seed the ruler.
          contextMenu: {
            enabled: true,
            line: { text: 'Marker', strokeDashArray: 5, color: '#0EA5E9' },
            items: ['annotate', 'xline', 'yline', 'measure'],
          },
          // Measure ruler + editable / draggable annotations. Both commit checkpoints,
          // so Undo / Redo steps through them alongside data and zoom changes.
          measure: { enabled: true },
          ink: { enabled: true, snap: true },
        },
        colors: ['#0EA5E9'],
        stroke: { width: 2.5, curve: 'smooth' },
        markers: { size: 0, hover: { size: 5 } },
        dataLabels: { enabled: false },
        title: {
          text: 'Undo / redo over data, zoom, annotations, and measurements',
          align: 'left',
        },
        xaxis: { type: 'numeric', title: { text: 'Session' }, tickAmount: 10 },
        yaxis: { decimalsInFloat: 0 },
        grid: { borderColor: '#eceff5' },
      },
      rewindTimer: null,
    }
  },
  mounted: function () {
    // The vue-apexcharts wrapper owns the render, so reach the live instance by
    // its chart.id, then wire the same buttons the vanilla build does.
    // (randomSeries lives in the shared head script.)
    var me = this
    me.rewindTimer = window.setInterval(function () {
      var chart = ApexCharts.getChartByID('rewind-demo')
      if (!chart) return
      window.clearInterval(me.rewindTimer)

      var undoBtn = document.getElementById('undo')
      var redoBtn = document.getElementById('redo')
      var readout = document.getElementById('readout')

      function syncButtons(state) {
        if (!state) state = chart.history.state()
        undoBtn.disabled = !state.canUndo
        redoBtn.disabled = !state.canRedo
        readout.innerHTML =
          'history: <b>' +
          (state.index + 1) +
          ' / ' +
          state.length +
          '</b> checkpoints'
      }

      chart.addEventListener('historyChange', function (c, state) {
        syncButtons(state && state.length != null ? state : undefined)
      })

      undoBtn.addEventListener('click', function () {
        chart.history.undo()
      })
      redoBtn.addEventListener('click', function () {
        chart.history.redo()
      })
      document
        .getElementById('randomize')
        .addEventListener('click', function () {
          chart.updateSeries([{ name: 'Value', data: randomSeries() }])
        })
      document.getElementById('reset').addEventListener('click', function () {
        chart.history.clear()
      })

      // initial state once the baseline checkpoint lands
      window.setTimeout(syncButtons, 300)
    }, 50)
  },
  beforeDestroy: function () {
    if (this.rewindTimer) window.clearInterval(this.rewindTimer)
  },
}
</script>

<style>
#chart {
  max-width: 760px;
  margin: 16px auto;
}
.panel {
  max-width: 760px;
  margin: 16px auto;
  font-family: sans-serif;
}
.controls {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 8px;
}
.controls button {
  padding: 7px 15px;
  cursor: pointer;
  border: 1px solid #cdd3de;
  background: #fff;
  border-radius: 6px;
  font-size: 14px;
}
.controls button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.controls .primary {
  border-color: #0ea5e9;
  color: #0ea5e9;
  font-weight: 600;
}
.readout {
  font-family: monospace;
  font-size: 13px;
  color: #444;
  margin-left: auto;
}
.readout b {
  color: #111;
}
.note {
  background: #f6f4fe;
  border-left: 3px solid #0ea5e9;
  padding: 10px 14px;
  font-size: 13px;
  color: #234;
  border-radius: 2px;
  margin-top: 12px;
  line-height: 1.55;
}
.note code {
  background: #e9e4fd;
  padding: 1px 5px;
  border-radius: 3px;
}
</style>