Line Chart Draw
A line chart draws itself from left to right, the area under it fades in, and each point pops just as the line reaches it.
How to use it
- Paste the snippet just before
</head>. - Add
data-line-chart-draw="…values…"to an element with a height.
Options
Add any of these attributes to the same element.
data-labels | Comma-separated labels shown under the chart. |
data-color | Line colour. Default #a855f7. |
data-duration | Time to draw the line. Default 1800 ms. |
data-loop | Redraw every few seconds. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Line Chart Draw: a line chart draws itself from left to right, the area
// under it fades in, and each point pops as the line reaches it.
// Usage: <div data-line-chart-draw="12,18,15,26,22,34,40" style="height:220px"></div>
// Optional: data-labels="Mon,Tue,…", data-color (default #a855f7),
// data-duration (ms), data-loop (redraw every few seconds).
var DEFAULTS = { color: '#a855f7', duration: 1800, hold: 2600 };
var uid = 0;
var css =
'.lcd{position:relative;display:flex;flex-direction:column}' +
'.lcd-plot{position:relative;flex:1;min-height:0}' +
'.lcd-plot svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}' +
'.lcd-dot{position:absolute;width:10px;height:10px;margin:-5px 0 0 -5px;border-radius:50%;background:#fff;border:2.5px solid var(--lcd-color);box-sizing:border-box}' +
'.lcd-labels{display:flex;justify-content:space-between;font-size:12px;opacity:.6;margin-top:8px}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function build(el) {
var d = el.dataset;
var values = el.getAttribute('data-line-chart-draw').split(',').map(Number);
var labels = d.labels ? d.labels.split(',') : [];
var color = d.color || DEFAULTS.color;
var id = 'lcd' + uid++;
var max = Math.max.apply(null, values), min = Math.min(0, Math.min.apply(null, values));
var n = values.length;
var pts = values.map(function (v, i) {
return [n > 1 ? (i / (n - 1)) * 100 : 50, 100 - ((v - min) / (max - min || 1)) * 92 - 4];
});
var line = pts.map(function (p, i) { return (i ? 'L' : 'M') + p[0] + ' ' + p[1]; }).join(' ');
var area = line + ' L100 100 L0 100 Z';
el.classList.add('lcd');
el.style.setProperty('--lcd-color', color);
el.setAttribute('role', 'img');
el.setAttribute('aria-label', 'Line chart: ' + values.map(function (v, i) { return (labels[i] ? labels[i] + ' ' : '') + v; }).join(', '));
el.innerHTML =
'<div class="lcd-plot" aria-hidden="true">' +
'<svg viewBox="0 0 100 100" preserveAspectRatio="none">' +
'<defs><linearGradient id="' + id + '" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="' + color + '" stop-opacity=".28"/><stop offset="1" stop-color="' + color + '" stop-opacity="0"/></linearGradient></defs>' +
[25, 50, 75].map(function (y) { return '<line x1="0" x2="100" y1="' + y + '" y2="' + y + '" stroke="currentColor" stroke-opacity=".1" vector-effect="non-scaling-stroke"/>'; }).join('') +
'<path class="lcd-area" d="' + area + '" fill="url(#' + id + ')"/>' +
'<path class="lcd-line" d="' + line + '" fill="none" stroke="' + color + '" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke"/>' +
'</svg></div>' +
(labels.length ? '<div class="lcd-labels" aria-hidden="true">' + labels.map(function (l) { return '<span>' + l.replace(/</g, '<') + '</span>'; }).join('') + '</div>' : '');
var plot = el.querySelector('.lcd-plot');
var dots = pts.map(function (p) {
var dot = document.createElement('span');
dot.className = 'lcd-dot';
dot.style.left = p[0] + '%';
dot.style.top = p[1] + '%';
plot.appendChild(dot);
return dot;
});
return { el: el, line: el.querySelector('.lcd-line'), area: el.querySelector('.lcd-area'), dots: dots };
}
function hide(c) {
anime.utils.set(anime.svg.createDrawable(c.line), { draw: '0 0' });
anime.utils.set(c.area, { opacity: 0 });
anime.utils.set(c.dots, { scale: 0 });
}
function draw(c) {
var d = c.el.dataset;
var duration = +d.duration || DEFAULTS.duration;
var n = c.dots.length;
anime.animate(anime.svg.createDrawable(c.line), { draw: ['0 0', '0 1'], duration: duration, ease: 'inOut(2)' });
anime.animate(c.area, { opacity: [0, 1], duration: duration * 0.8, delay: duration * 0.3, ease: 'out(2)' });
anime.animate(c.dots, {
scale: [0, 1],
duration: 500,
// Pop each point roughly when the line reaches it.
delay: function (_, i) { return n > 1 ? (i / (n - 1)) * duration * 0.85 : 0; },
ease: 'outBack(2.5)'
});
if (d.loop !== undefined && d.loop !== 'false') {
setTimeout(function () {
anime.animate(c.dots, { scale: 0, duration: 250, delay: anime.stagger(30) });
anime.animate(c.area, { opacity: 0, duration: 300 });
anime.animate(anime.svg.createDrawable(c.line), {
draw: '1 1', duration: 600, ease: 'in(2)',
onComplete: function () { hide(c); draw(c); }
});
}, duration + DEFAULTS.hold);
}
}
function init() {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
draw(e.target._lcd);
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-line-chart-draw]').forEach(function (el) {
if (el._lcd) return;
var c = (el._lcd = build(el));
if (reduced) return;
hide(c);
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-line-chart-draw="12,18,15,26,22,34,40"
data-labels="Mon,Tue,Wed,Thu,Fri,Sat,Sun"
style="height: 220px"></div>