recon/static/js/charts.js
Matt 563c16bb71 Initial commit: RECON codebase baseline
Current state of the pipeline code as of 2026-04-14 (Phase 1 scaffolding complete).
Config has new_pipeline.enabled=false and crawler.sites=[] per refactor plan.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 14:57:23 +00:00

186 lines
6.6 KiB
JavaScript

/* RECON Lightweight Canvas Line Chart
* No dependencies. drawLineChart(canvasId, datasets, opts)
* DPI-aware rendering for sharp lines on all displays.
*/
var ReconChart = (function() {
'use strict';
var COLORS = ['#00ff41', '#0ea5e9', '#ffa500', '#ff4444', '#7c3aed', '#fbbf24'];
function drawLineChart(canvasId, datasets, opts) {
opts = opts || {};
var canvas = document.getElementById(canvasId);
if (!canvas) return;
// DPI-aware sizing — match canvas bitmap to actual CSS pixels
var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
var cssW = rect.width || 800;
var cssH = rect.height || 200;
canvas.width = cssW * dpr;
canvas.height = cssH * dpr;
var ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
var W = cssW;
var H = cssH;
var pad = {top: 20, right: 20, bottom: 30, left: 60};
var plotW = W - pad.left - pad.right;
var plotH = H - pad.top - pad.bottom;
// Clear
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, W, H);
if (!datasets || datasets.length === 0) {
ctx.fillStyle = '#666';
ctx.font = '12px Courier New';
ctx.textAlign = 'center';
ctx.fillText('No data', W/2, H/2);
return;
}
// Find global min/max Y
var allY = [];
var allX = [];
datasets.forEach(function(ds) {
ds.points.forEach(function(p) {
allY.push(p.y);
allX.push(p.x);
});
});
if (allY.length === 0) return;
var minY = Math.min.apply(null, allY);
var maxY = Math.max.apply(null, allY);
var minX = Math.min.apply(null, allX);
var maxX = Math.max.apply(null, allX);
// Add 10% padding to Y
var yRange = maxY - minY || 1;
minY = Math.max(0, minY - yRange * 0.05);
maxY = maxY + yRange * 0.1;
var xRange = maxX - minX || 1;
function xToCanvas(x) { return pad.left + ((x - minX) / xRange) * plotW; }
function yToCanvas(y) { return pad.top + plotH - ((y - minY) / (maxY - minY)) * plotH; }
// Grid lines
ctx.strokeStyle = '#222';
ctx.lineWidth = 1;
var ySteps = 5;
for (var i = 0; i <= ySteps; i++) {
var yVal = minY + (maxY - minY) * (i / ySteps);
var cy = yToCanvas(yVal);
ctx.beginPath();
ctx.moveTo(pad.left, cy);
ctx.lineTo(W - pad.right, cy);
ctx.stroke();
// Y labels
ctx.fillStyle = '#666';
ctx.font = '10px Courier New';
ctx.textAlign = 'right';
ctx.fillText(Math.round(yVal).toLocaleString(), pad.left - 6, cy + 3);
}
// X labels (time)
ctx.textAlign = 'center';
ctx.fillStyle = '#666';
var xSteps = Math.min(6, allX.length);
for (var j = 0; j < xSteps; j++) {
var xVal = minX + xRange * (j / (xSteps - 1 || 1));
var cx = xToCanvas(xVal);
var d = new Date(xVal);
var label = d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0');
ctx.fillText(label, cx, H - 8);
}
// Draw lines + dots at each data point
datasets.forEach(function(ds, idx) {
var color = ds.color || COLORS[idx % COLORS.length];
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
var pts = ds.points.sort(function(a, b) { return a.x - b.x; });
pts.forEach(function(p, i) {
var x = xToCanvas(p.x);
var y = yToCanvas(p.y);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
// Draw dots at each point for visibility with sparse data
ctx.fillStyle = color;
pts.forEach(function(p) {
var x = xToCanvas(p.x);
var y = yToCanvas(p.y);
ctx.beginPath();
ctx.arc(x, y, 3, 0, Math.PI * 2);
ctx.fill();
});
// Legend label
if (ds.label) {
ctx.fillStyle = color;
ctx.font = '10px Courier New';
ctx.textAlign = 'left';
ctx.fillText(ds.label, pad.left + idx * 100, 12);
}
});
}
function loadAndDraw(canvasId, metricType, keys, labels, hours) {
hours = hours || 24;
RECON.fetchJSON('/api/metrics/history?type=' + metricType + '&hours=' + hours).then(function(data) {
if (!data.points || data.points.length < 2) {
// Show "collecting data" message instead of hiding
var canvas = document.getElementById(canvasId);
if (!canvas) return;
var container = canvas.parentElement;
if (container) container.style.display = 'block';
var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
canvas.width = (rect.width || 800) * dpr;
canvas.height = (rect.height || 200) * dpr;
var ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, rect.width, rect.height);
ctx.fillStyle = '#555';
ctx.font = '12px Courier New';
ctx.textAlign = 'center';
var msg = data.points && data.points.length === 1
? 'Collecting data... (1 snapshot, need 2+)'
: 'Collecting data... (snapshots every 2 min)';
ctx.fillText(msg, (rect.width || 800) / 2, (rect.height || 200) / 2);
return;
}
var container = document.getElementById(canvasId).parentElement;
if (container) container.style.display = 'block';
var datasets = keys.map(function(key, i) {
return {
label: labels[i] || key,
color: COLORS[i % COLORS.length],
points: data.points.map(function(p) {
return {
x: new Date(p.timestamp).getTime(),
y: p.data[key] || 0
};
})
};
});
drawLineChart(canvasId, datasets);
}).catch(function() {});
}
return {
drawLineChart: drawLineChart,
loadAndDraw: loadAndDraw
};
})();