'use strict'; window.addEventListener('DOMContentLoaded', function () { Object.defineProperties(GigabaitChartController, { onloaded: {enumerable: true, value: true}, }); }); function GigabaitChartController(Values, Options) { Options = (Options || {}); if (!(this instanceof GigabaitChartController)) { return new GigabaitChartController(Values, Options); } // Constants ---------------------------------------------------------------- // let SVGViewBox = '0 0 ' + (Options.Aspect || '16 9'); let AxisKeyX = Options.AxisKeyX || 'timestamp'; let AxisKeyY = Options.AxisKeyY || 'server_online'; let InfoBg = Options.InfoBg || 'rgba(255, 255, 255, .75)'; let InfoColor = Options.InfoColor || '#222222'; let Transition = ['all 100ms ease'].join(', '); let DateFormat = Options.DateFormat || null; // Властивості ------------------------------------------------------------- // let Root, SVG, Chart, InfoWrap, InfoContent, CursorVertical; // Ініціалізація ----------------------------------------------------------- // (function Construct() { FindNeedle(function () { UseChart(); UseSVG(); UseInfo(); DrawChart(); }); })(); // Методи ------------------------------------------------------------------ // function FindNeedle(callback) { if (Root) return; let interval = setInterval(function () { Root = document.querySelector('#statistics'); InfoContent = document.querySelector('.svgchart-info-content-template'); if (Root && InfoContent) { clearInterval(interval); callback(); } }, 66); } function UseChart() { Chart = { values: Values.slice(), minX: Infinity, maxX: -Infinity, minY: 0, maxY: -Infinity }; for (let rec of Chart.values) { let x = parseFloat(rec[AxisKeyX]); let y = parseFloat(rec[AxisKeyY]); Chart.minX = Math.min(Chart.minX, x); Chart.maxX = Math.max(Chart.maxX, x); Chart.maxY = Math.max(Chart.maxY, y); } Chart.maxY = Math.ceil((Chart.maxY * 1.1) / 10) * 10; } function UseSVG() { SVG = CreateSVGElement('svg') .Set({viewBox: SVGViewBox}) .SetStyle({display: 'inherit', cursor: 'none'}); Root.appendChild(SVG); } function UseInfo() { InfoWrap = document.createElement('div'); Object.assign(InfoWrap.style, { position: 'fixed', transform: 'scale(0)', padding: '0', borderRadius: '2px', backgroundColor: InfoBg, color: InfoColor, whiteSpace: 'nowrap', pointerEvents: 'none', transition: Transition, opacity: 0, zIndex: 100000, }); InfoContent.parentElement.removeChild(InfoContent); InfoContent.style.removeProperty('display'); InfoWrap.appendChild(InfoContent); Root.appendChild(InfoWrap); // Приховати при виході курсора SVG.addEventListener('mouseleave', function () { Object.assign(InfoWrap.style, { transform: 'scale(0)', padding: '0', opacity: 0, }); }); } function DrawChart() { let vb = SVG.viewBox.baseVal; let minBoundSize = Math.min(vb.width, vb.height); let XSpace = Options.ShowHorizontalSegments ? 0.125 : 0; // Групи let GroupMain = CreateSVGElement('g') .SetStyle({transform: 'scale(0.95) translateY(-5%)', transformOrigin: 'center center'}); let GroupPath = CreateSVGElement('g') .SetStyle({transform: `translateX(${(XSpace * 100) * .6}%)`, transformOrigin: 'center center'}); SVG.Append(GroupMain); GroupMain.Append(GroupPath); // Вертикальний курсор CursorVertical = CreateSVGElement('rect') .Set({ x: 0, y: 0, width: minBoundSize * 0.01, height: '100%', fill: InfoBg, opacity: 0, filter: `drop-shadow(0 0 4px ${InfoBg})` }) .SetStyle({pointerEvents: 'none', transition: 'all 20ms linear'}); SVG.Append(CursorVertical); SVG.addEventListener('mousemove', function (e) { let br = SVG.getBoundingClientRect(); let left = (((e.clientX - br.left) / br.width) * 100) + '%'; CursorVertical.Set({x: left}).SetStyle({opacity: .5}); }); SVG.addEventListener('mouseleave', function () { CursorVertical.SetStyle({opacity: 0}); }); // Шлях let Path = DrawPath() .Set({ stroke: Options.LineColor || 'dodgerblue', 'stroke-width': minBoundSize * 0.02, 'stroke-linejoin': 'round', 'stroke-linecap': 'round', fill: 'transparent' }); GroupPath.Append(Path); // Точки + інтерактивні області let total = Chart.values.length; let rectWpct = ((1 / total) * (1 - XSpace)) * 100; for (let i = 0; i < total; i++) { let rec = Chart.values[i]; let xN = (rec[AxisKeyX] - Chart.minX) / (Chart.maxX - Chart.minX); let yN = 1 - (rec[AxisKeyY] - Chart.minY) / (Chart.maxY - Chart.minY); let xP = xN * (1 - XSpace) * vb.width; let yP = yN * vb.height; // лінія Path.AddPoint(xP, yP); // точка let point = CreateSVGElement('circle') .Set({cx: xP, cy: yP, r: minBoundSize * 0.025, fill: Options.PointColor || '#18bc9b'}) .SetStyle({transition: Transition}); point.rSrc = parseFloat(point.Get('r')); GroupPath.Append(point); // інтерактивна зона let region = CreateSVGElement('rect') .Set({width: rectWpct + '%', height: '100%', x: `${rectWpct * i}%`, y: 0, fill: 'transparent'}); region.dataIndex = i; region.point = point; GroupPath.Append(region); region.addEventListener('mouseenter', function () { let r = Chart.values[this.dataIndex]; let onlineEl = InfoContent.querySelector('*[svg-data-type=online]'); let timeEl = InfoContent.querySelector('*[svg-data-type=time]'); if (onlineEl) onlineEl.innerHTML = r[AxisKeyY]; // тут обробляємо формат дати: if (timeEl) { let ts = r[AxisKeyX]; timeEl.innerHTML = FormatDate(ts); } // показуємо блок Object.assign(InfoWrap.style, {transform: 'scale(1)', padding: '10px', opacity: 1}); let pB = this.point.getBoundingClientRect(); let iB = InfoContent.getBoundingClientRect(); let left = pB.left - iB.width / 2; left = Math.max(0, Math.min(left, window.innerWidth - iB.width * 1.5)); Object.assign(InfoWrap.style, {top: (pB.top + 16) + 'px', left: left + 'px'}); SetActivePoint(this.point); }); } function SetActivePoint(pt) { ActivePoint && ActivePoint.Set({r: ActivePoint.rSrc}); if (pt) { pt.Set({r: pt.rSrc * 2}); ActivePoint = pt; } } // горизонтальні сегменти if (Options.ShowHorizontalSegments) { let count = Math.max(4, Math.floor(Root.getBoundingClientRect().height / 100)); let sizeP = ((Chart.maxY / count) / Chart.maxY) * 100; for (let i = 0; i < count; i++) { let seg = CreateSVGElement('rect') .Set({ x: 0, y: `${100 - sizeP * i}%`, width: '100%', height: minBoundSize * 0.01, fill: InfoBg, opacity: .25, filter: `drop-shadow(0 0 4px ${InfoBg})` }) .SetStyle({pointerEvents: 'none'}); GroupMain.Append(seg, -1); let txt = CreateSVGElement('text') .Set({x: 0, y: `${100 - (sizeP * i + 3)}%`, 'font-size': '75%', fill: InfoBg}); let val = (Chart.maxY / count) * i; txt.textContent = val >= 1000 ? (val / 1000).toFixed(0) + 'k' : val.toFixed(0); GroupMain.Append(txt); } } } // Форматуємо timestamp (у секундах) за PHP-подібним патерном function FormatDate(ts) { let d = new Date(ts * 1000); if (!DateFormat) { // fallback: ISO без мілісекунд return d.toISOString().replace('T', ' ').split('.')[0]; } let pad2 = n => String(n).padStart(2, '0'); let map = { H: pad2(d.getHours()), i: pad2(d.getMinutes()), s: pad2(d.getSeconds()), d: pad2(d.getDate()), m: pad2(d.getMonth() + 1), Y: d.getFullYear() }; return DateFormat.replace(/H|i|s|d|m|Y/g, t => map[t] || t); } // SVG-утиліти ------------------------------------------------------------- // function CreateSVGElement(name) { if (!name) throw new Error('Invalid SVG element name'); let E = document.createElementNS('http://www.w3.org/2000/svg', name); Object.defineProperties(E, { Set: { value: function (attr, val) { if (typeof attr === 'object') { for (let k in attr) this.Set(k, attr[k]); } else if (val == null) { this.removeAttribute(attr); } else { this.setAttribute(attr, val); } return this; } }, SetStyle: { value: function (styles) { Object.assign(this.style, styles); return this; } }, Get: { value: function (attr) { return this.getAttribute(attr); } }, Append: { value: function (el, before) { if (Array.isArray(el)) { el.forEach(x => this.Append(x, before)); } else { if (before === -1) before = this.childNodes[0]; this.insertBefore(el, before || null); } return this; } } }); return E; } function DrawPath() { let P = CreateSVGElement('path'); Object.defineProperties(P, { points: {value: []}, AddPoint: { value: function (x, y) { P.points.push([x, y]); return P.Render(); } }, Render: { value: function () { if (!P.points.length) return P; let cmds = P.points.map((p, i) => { let [x, y] = p; return i === 0 ? `M ${x} ${y} C ${x} ${y}` : `${x} ${y} ${x} ${y} ${x} ${y}`; }); cmds.push('Z'); P.Set('d', cmds.join(' ')); return P; } } }); return P; } }