Files
OTSSigns-Website/theme/assets/js/dashboard-animator.js

161 lines
6.3 KiB
JavaScript
Raw Normal View History

/**
* Dashboard Chart Animator
* Continuously animates SVG bar charts, line graph, and pie chart.
* Uses requestAnimationFrame, pauses off-screen via IntersectionObserver.
*/
(function () {
'use strict';
/* ── Configuration ──────────────────────────────────── */
var SPEED = 0.0025; // phase increment per ms (~4-6 s visible cycle)
var BAR_H = 120; // max bar height in SVG units
var BAR_MIN = 0.12; // minimum bar height ratio (12 %)
var LINE_PTS = 8; // number of points on the line graph
/* ── Colour sets (matches theme tokens) ─────────────── */
var DARK = { text: '#E0E0E0', muted: '#9E9E9E', border: '#333', center: '#222' };
var LIGHT = { text: '#333333', muted: '#666666', border: '#E0E0E0', center: '#fff' };
function isDark() {
return document.documentElement.getAttribute('data-theme') === 'dark';
}
function palette() { return isDark() ? DARK : LIGHT; }
/** Stacked sine waves => smooth organic value in 0..1 range */
function wave(t, offset) {
return Math.max(0, Math.min(1,
0.55 +
Math.sin(t * 1.0 + offset) * 0.30 +
Math.sin(t * 2.3 + offset * 1.4) * 0.25 +
Math.sin(t * 0.7 + offset * 0.6) * 0.20
));
}
/* ── Per-chart state ────────────────────────────────── */
function makeState(svg) {
return {
svg: svg,
bars1: svg.querySelectorAll('#bars-group-1 .bar'),
bars2: svg.querySelectorAll('#bars-group-2 .bar'),
vals1: svg.querySelectorAll('#values-group-1 text'),
vals2: svg.querySelectorAll('#values-group-2 text'),
linePath: svg.querySelector('#line-path'),
lineFill: svg.querySelector('#line-fill'),
pieSegs: svg.querySelectorAll('.pie-segment'),
phase: Math.random() * Math.PI * 2,
paused: false,
raf: null,
themeN: 0
};
}
/* ── Update routines ────────────────────────────────── */
function updateBars(bars, vals, st, pct) {
var i, v, h;
for (i = 0; i < bars.length; i++) {
v = wave(st.phase, i * 1.1);
v = Math.max(BAR_MIN, v);
h = v * BAR_H;
bars[i].setAttribute('height', h);
bars[i].setAttribute('y', BAR_H - h);
}
for (i = 0; i < Math.min(bars.length, vals.length); i++) {
v = wave(st.phase, i * 1.1);
v = Math.max(BAR_MIN, v);
vals[i].textContent = pct ? Math.round(v * 100) + '%' : Math.round(v * 5000);
}
}
function updateLine(st) {
if (!st.linePath) return;
var d = 'M', i, x, y, v;
for (i = 0; i < LINE_PTS; i++) {
x = (i / (LINE_PTS - 1)) * 200;
v = wave(st.phase * 0.8, i * 0.9);
y = 25 + (1 - v) * 110;
d += (i ? ' L' : '') + x.toFixed(1) + ',' + y.toFixed(1);
}
st.linePath.setAttribute('d', d);
if (st.lineFill) st.lineFill.setAttribute('d', d + ' L200,145 L0,145 Z');
}
function updatePie(st) {
if (!st.pieSegs.length) return;
var base = (st.phase * 40) % 360;
for (var i = 0; i < st.pieSegs.length; i++) {
var w = Math.sin(st.phase * 0.6 + i * 1.5) * 4;
st.pieSegs[i].setAttribute('transform', 'rotate(' + (base + i * 90 + w) + ')');
}
}
function applyTheme(st) {
var c = palette();
var q = st.svg.querySelectorAll.bind(st.svg);
var all, j;
all = q('.ct'); for (j = 0; j < all.length; j++) all[j].setAttribute('fill', c.text);
all = q('.cl'); for (j = 0; j < all.length; j++) all[j].setAttribute('fill', c.muted);
all = q('.cv'); for (j = 0; j < all.length; j++) all[j].setAttribute('fill', c.text);
all = q('.grid-line'); for (j = 0; j < all.length; j++) all[j].setAttribute('stroke', c.border);
var cc = st.svg.querySelector('#pie-center');
if (cc) { cc.setAttribute('fill', c.center); cc.setAttribute('stroke', c.border); }
var pt = st.svg.querySelector('#pie-center-text');
if (pt) pt.setAttribute('fill', c.text);
}
/* ── Animation loop ─────────────────────────────────── */
function tick(st) {
if (!st.paused) {
st.phase += SPEED * 16; // ~16 ms per frame at 60 fps
updateBars(st.bars1, st.vals1, st, true);
updateBars(st.bars2, st.vals2, st, false);
updateLine(st);
updatePie(st);
// Re-apply theme colours every ~30 frames (~0.5 s)
if (++st.themeN > 30) { st.themeN = 0; applyTheme(st); }
}
st.raf = requestAnimationFrame(function () { tick(st); });
}
/* ── Hover pause / resume ───────────────────────────── */
function attachHover(st) {
var el = st.svg.closest('[data-dashboard-container]') || st.svg.parentElement;
if (!el) return;
el.addEventListener('mouseenter', function () { st.paused = true; });
el.addEventListener('mouseleave', function () { st.paused = false; });
el.addEventListener('touchstart', function () { st.paused = true; }, { passive: true });
el.addEventListener('touchend', function () { st.paused = false; }, { passive: true });
}
/* ── IntersectionObserver (pause off-screen) ────────── */
function observe(st) {
if (!('IntersectionObserver' in window)) return;
new IntersectionObserver(function (entries) {
for (var i = 0; i < entries.length; i++) st.paused = !entries[i].isIntersecting;
}, { rootMargin: '200px', threshold: 0.05 }).observe(st.svg);
}
/* ── Bootstrap ──────────────────────────────────────── */
function boot() {
var svgs = document.querySelectorAll('.dashboard-chart');
if (!svgs.length) return;
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
for (var i = 0; i < svgs.length; i++) {
if (svgs[i]._dbAnim) continue;
var st = makeState(svgs[i]);
svgs[i]._dbAnim = st;
applyTheme(st);
if (!reduced) { tick(st); attachHover(st); observe(st); }
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();