<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: { history: { enabled: true } }</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'
// 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>