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> · Currently drawn: <b>' +
drawn.toLocaleString() +
'</b> · Window: <b>' +
lastZoomLabel +
'</b> · Render: <b>' +
lastRenderMs +
' ms</b>'
}
var options = {
series: [{ name: 'Sensor reading', data: seriesData }],
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' } },
}
var chart = new ApexCharts(document.querySelector('#chart'), options)
chart.render()
// Initial render is started by the template above; wait one frame and update
// stats once the chart instance has finished its first paint.
requestAnimationFrame(function poll() {
if (chart.w && chart.w.globals && chart.w.globals.series) {
updateStats(chart)
} else {
requestAnimationFrame(poll)
}
})
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()
chart.destroy()
chart = new ApexCharts(document.querySelector('#chart'), options)
chart.render().then(function () {
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)
})