Latency Distribution in JavaScript
Using ApexCharts with JavaScript
This Latency Distribution example uses ApexCharts.js directly in JavaScript, with no wrapper component.
Install it with npm install apexcharts, then mount the chart with new ApexCharts(element, options).render().
// Shared by the vanilla-js, React and Vue builds.
//
// The series carries RAW OBSERVATIONS, one number per request, and the chart
// bins them itself. That is the whole point of the type: you hand it a sample,
// not a pre-aggregated table.
//
// 1800 response times with the shape real latency has: a tight body around
// 120ms and a long right tail of slow requests. Generated from a seeded
// generator so the page is deterministic.
var LATENCY = (function () {
var seed = 20260811
function rand() {
seed = (seed * 16807) % 2147483647
return (seed - 1) / 2147483646
}
var out = []
for (var i = 0; i < 1800; i++) {
// Box-Muller into a log-normal: the standard shape for service latency.
var u1 = Math.max(rand(), 1e-9)
var u2 = rand()
var z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2)
out.push(Math.round(Math.exp(4.8 + z * 0.42) * 10) / 10)
}
return out
})()
function percentile(values, p) {
var sorted = values.slice().sort(function (a, b) {
return a - b
})
var pos = (sorted.length - 1) * p
var lo = Math.floor(pos)
var hi = Math.ceil(pos)
if (lo === hi) return sorted[lo]
return sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo)
}
function renderStats() {
var el = document.querySelector('#stats')
if (!el) return
el.innerHTML =
'<b>' +
LATENCY.length +
'</b> requests · ' +
'median <b>' +
percentile(LATENCY, 0.5).toFixed(0) +
' ms</b> · ' +
'p95 <b>' +
percentile(LATENCY, 0.95).toFixed(0) +
' ms</b> · ' +
'slowest <b>' +
Math.max.apply(null, LATENCY).toFixed(0) +
' ms</b>'
}
// Wires the bin-rule buttons and the cumulative toggle to a live chart. Shared
// by all three framework builds.
function wireHistogramControls(chart) {
var buttons = [].slice.call(document.querySelectorAll('[data-bins]'))
var cumulative = false
var bins = 'auto'
function apply(next) {
buttons.forEach(function (b) {
b.className = b.getAttribute('data-bins') === String(next) ? 'on' : ''
})
bins = next === 'auto' ? 'auto' : parseInt(next, 10)
chart.updateOptions({
plotOptions: { histogram: { bins: bins, cumulative: cumulative } },
yaxis: {
title: { text: cumulative ? 'Requests (cumulative)' : 'Requests' },
},
})
}
buttons.forEach(function (b) {
b.addEventListener('click', function () {
apply(b.getAttribute('data-bins'))
})
})
var cb = document.querySelector('#cumulative')
if (cb) {
cb.addEventListener('change', function (e) {
cumulative = e.target.checked
apply(bins === 'auto' ? 'auto' : String(bins))
})
}
renderStats()
}
var options = {
series: [
{
name: 'Requests',
data: LATENCY,
},
],
chart: {
id: 'latency',
type: 'histogram',
// Fixed width, not responsive: 35 thin bars make every bar edge a hairline,
// so a page-width nudge of a pixel or two (a scrollbar appearing as the
// stats line renders) visibly moves the whole distribution.
width: 700,
height: 400,
toolbar: {
show: false,
},
},
plotOptions: {
histogram: {
// 'auto' takes the narrower of Freedman-Diaconis and Sturges. Swap in a
// number for a fixed bin count, or binWidth for fixed boundaries.
bins: 'auto',
},
},
colors: ['#008FFB'],
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 0.25,
opacityFrom: 0.95,
opacityTo: 0.75,
stops: [0, 100],
},
},
legend: {
show: false,
},
xaxis: {
title: {
text: 'Response time (ms)',
},
labels: {
formatter: function (val) {
return Math.round(val)
},
},
},
yaxis: {
title: {
text: 'Requests',
},
},
}
var chart = new ApexCharts(document.querySelector('#chart'), options)
chart.render()
// LATENCY and wireHistogramControls live in the shared head script.
wireHistogramControls(chart)