<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><canvas></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'
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> · Active renderer: <b>' +
active.toUpperCase() +
'</b> · DOM nodes in chart: <b>' +
domNodeCount().toLocaleString() +
'</b> · 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>