u
This commit is contained in:
@@ -177,7 +177,7 @@
|
||||
<!-- Brightness Chart -->
|
||||
<div id="chartContainer"
|
||||
style="margin-top: 40px; background: var(--card-bg); padding: 20px; border-radius: 8px; display: none;">
|
||||
<h3>Brightness Levels</h3>
|
||||
<h3 id="chartTitle">Brightness Levels</h3>
|
||||
<canvas id="brightnessChart" style="width: 100%; height: 200px;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,7 +193,8 @@
|
||||
const state = {
|
||||
cameras: [],
|
||||
selectedCamera: localStorage.getItem('lastCamera') || '',
|
||||
selectedDate: localStorage.getItem('lastDate') || ''
|
||||
selectedDate: localStorage.getItem('lastDate') || '',
|
||||
currentSettings: {}
|
||||
};
|
||||
|
||||
// DOM Elements
|
||||
@@ -258,6 +259,7 @@
|
||||
|
||||
if (state.selectedCamera) {
|
||||
loadDates(state.selectedCamera);
|
||||
loadSettings(state.selectedCamera);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -299,6 +301,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings(cameraId) {
|
||||
try {
|
||||
const res = await fetch(`settings/${cameraId}`, { headers: getHeaders() });
|
||||
const data = await res.json();
|
||||
state.currentSettings = data.settings || {};
|
||||
} catch (err) {
|
||||
console.error('Failed to load settings', err);
|
||||
state.currentSettings = {};
|
||||
}
|
||||
}
|
||||
|
||||
async function loadImages(isAutoUpdate = false) {
|
||||
const cameraId = cameraSelect.value;
|
||||
const dateStr = dateSelect.value; // YYYY-MM-DD
|
||||
@@ -439,16 +452,49 @@
|
||||
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
const padding = { top: 20, right: 20, bottom: 30, left: 40 };
|
||||
const padding = { top: 20, right: 30, bottom: 30, left: 45 };
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// Sort stats by timestamp just in case
|
||||
stats.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
||||
|
||||
// Determine data source and range
|
||||
// If any entry has 'ocr_val', prefer that.
|
||||
const hasOcr = stats.some(s => s.ocr_val !== undefined && s.ocr_val !== null);
|
||||
const valueKey = hasOcr ? 'ocr_val' : 'brightness';
|
||||
|
||||
// Clean data (filter nulls)
|
||||
const validPoints = stats
|
||||
.filter(s => s[valueKey] !== undefined && s[valueKey] !== null)
|
||||
.map(s => ({
|
||||
t: s.timestamp,
|
||||
v: Number(s[valueKey])
|
||||
}));
|
||||
|
||||
if (validPoints.length === 0) return;
|
||||
|
||||
let minY = Math.min(...validPoints.map(p => p.v));
|
||||
let maxY = Math.max(...validPoints.map(p => p.v));
|
||||
|
||||
// Add some padding to Y range
|
||||
const range = maxY - minY;
|
||||
if (range === 0) {
|
||||
minY -= 10;
|
||||
maxY += 10;
|
||||
} else {
|
||||
minY -= range * 0.1;
|
||||
maxY += range * 0.1;
|
||||
}
|
||||
// For brightness, clamp to 0-255 if not OCR?
|
||||
if (!hasOcr) {
|
||||
minY = 0;
|
||||
maxY = 255;
|
||||
}
|
||||
|
||||
// Helper to parsing time to minutes from start of day (local time)
|
||||
const getMinutes = (iso) => {
|
||||
// Parse UCT timestamp to Date object
|
||||
// Parse UTC timestamp to Date object
|
||||
const parts = iso.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})[-:](\d{2})[-:](\d{2})/);
|
||||
let d;
|
||||
if (parts) {
|
||||
@@ -460,14 +506,15 @@
|
||||
return d.getHours() * 60 + d.getMinutes();
|
||||
};
|
||||
|
||||
const points = stats.map(s => ({
|
||||
x: getMinutes(s.timestamp),
|
||||
y: s.brightness
|
||||
const points = validPoints.map(p => ({
|
||||
x: getMinutes(p.t),
|
||||
y: p.v
|
||||
}));
|
||||
|
||||
// Scales
|
||||
const mapX = (minutes) => padding.left + (minutes / 1440) * (width - padding.left - padding.right);
|
||||
const mapY = (val) => height - padding.bottom - (val / 255) * (height - padding.top - padding.bottom);
|
||||
// Invert Y (0 at bottom)
|
||||
const mapY = (val) => height - padding.bottom - ((val - minY) / (maxY - minY)) * (height - padding.top - padding.bottom);
|
||||
|
||||
// Draw Axes
|
||||
ctx.strokeStyle = '#555';
|
||||
@@ -486,7 +533,7 @@
|
||||
ctx.stroke();
|
||||
|
||||
// Draw Line
|
||||
ctx.strokeStyle = '#3b82f6';
|
||||
ctx.strokeStyle = hasOcr ? '#10b981' : '#3b82f6'; // Green for OCR, Blue for Brightness
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
|
||||
@@ -498,6 +545,14 @@
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
// Draw Dots
|
||||
ctx.fillStyle = hasOcr ? '#10b981' : '#3b82f6';
|
||||
for (const p of points) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(mapX(p.x), mapY(p.y), 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Draw Labels (Time)
|
||||
ctx.fillStyle = '#ccc';
|
||||
ctx.font = '12px sans-serif';
|
||||
@@ -505,16 +560,35 @@
|
||||
|
||||
for (let h = 0; h <= 24; h += 4) {
|
||||
const x = mapX(h * 60);
|
||||
ctx.fillText(`${h}:00`, x, height - 10);
|
||||
if (x > padding.left) {
|
||||
ctx.fillText(`${h}:00`, x, height - 10);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw Labels (Brightness)
|
||||
// Draw Labels (Value)
|
||||
ctx.textAlign = 'right';
|
||||
ctx.textBaseline = 'middle';
|
||||
for (let b = 0; b <= 255; b += 64) {
|
||||
const y = mapY(b);
|
||||
ctx.fillText(Math.round(b), padding.left - 5, y);
|
||||
// Draw 5 ticks
|
||||
for (let i = 0; i <= 4; i++) {
|
||||
const val = minY + (i / 4) * (maxY - minY);
|
||||
const y = mapY(val);
|
||||
ctx.fillText(Math.round(val), padding.left - 5, y);
|
||||
}
|
||||
|
||||
// Title
|
||||
ctx.save();
|
||||
ctx.translate(15, height / 2);
|
||||
ctx.rotate(-Math.PI / 2);
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textAlign = 'center';
|
||||
const label = state.currentSettings.chartLabel || (hasOcr ? "Value" : "Brightness");
|
||||
ctx.fillText(label, 0, 0);
|
||||
|
||||
// Update Header too
|
||||
const titleEl = document.getElementById('chartTitle');
|
||||
if (titleEl) titleEl.textContent = label;
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function getPrettyTime(ts) {
|
||||
@@ -541,8 +615,10 @@
|
||||
// Listeners
|
||||
cameraSelect.addEventListener('change', (e) => {
|
||||
state.selectedCamera = e.target.value;
|
||||
if (state.selectedCamera) loadDates(state.selectedCamera);
|
||||
else dateSelect.disabled = true;
|
||||
if (state.selectedCamera) {
|
||||
loadDates(state.selectedCamera);
|
||||
loadSettings(state.selectedCamera);
|
||||
} else dateSelect.disabled = true;
|
||||
});
|
||||
|
||||
dateSelect.addEventListener('change', (e) => {
|
||||
|
||||
Reference in New Issue
Block a user