Speed history: rework chart and add live stats
- Replace 8-point scatter chart with proper 60-point line chart
- Blue gradient fill (rgba 35%→2%), tension 0.4 smooth curve
- No legend (obvious), no X axis grid, soft Y axis gridlines
- Y axis auto-scales: max(60, sessionMax*1.2) rounded to 10s
- Ticks: '0 km/h' labels on Y, time strings on X (max 6)
- chart.update('none') for live updates (no animation stutter)
- Speed history card header now shows current speed + session max
even when the graph is collapsed
- Footer bar inside expanded graph: session avg, point count, last reading time
- makeGradient() refreshed on every update in case canvas was resized
- obd_rpm now appends ' rpm' to the displayed value
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
+128
-61
@@ -1,120 +1,187 @@
|
||||
const socket = io();
|
||||
// map
|
||||
|
||||
// Offline tiles pre-downloaded via scripts/download_map_tiles.py (see PLAN.md
|
||||
// Phase 3). Only zoom 8-15 is cached by default - matches DEFAULT_MIN_ZOOM/
|
||||
// DEFAULT_MAX_ZOOM in that script.
|
||||
// Generic placeholder center (not a real location) - re-centers automatically
|
||||
// to the real live GPS fix once one comes in (see gps_location handler below).
|
||||
// ── Map ────────────────────────────────────────────────────────────────────
|
||||
var map = L.map('map', { minZoom: 8, maxZoom: 15 }).setView([48.505, -0.09], 12);
|
||||
const mapTiles = L.tileLayer('/static/vendor/tiles/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.maptiler.com/copyright/" target="_blank">MapTiler</a> © <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a> contributors',
|
||||
minZoom: 8,
|
||||
maxZoom: 15
|
||||
});
|
||||
var carMarker = L.marker(map.getCenter()).addTo(map)
|
||||
.bindPopup('your car... soon(tm)');
|
||||
const mapTiles = L.tileLayer('/static/vendor/tiles/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.maptiler.com/copyright/" target="_blank">MapTiler</a> © <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a> contributors',
|
||||
minZoom: 8,
|
||||
maxZoom: 15,
|
||||
});
|
||||
var carMarker = L.marker(map.getCenter()).addTo(map).bindPopup('Your Yaris');
|
||||
|
||||
// charts
|
||||
// ── Speed history chart ────────────────────────────────────────────────────
|
||||
const MAX_SPEED_POINTS = 60; // ~5 min at typical OBD poll rate
|
||||
|
||||
const ctx = document.getElementById('speedGraph').getContext('2d');
|
||||
chart = new Chart(ctx, {
|
||||
|
||||
// Gradient fill (created after canvas is sized)
|
||||
function makeGradient() {
|
||||
const grad = ctx.createLinearGradient(0, 0, 0, ctx.canvas.clientHeight || 200);
|
||||
grad.addColorStop(0, 'rgba(78,115,223,0.35)');
|
||||
grad.addColorStop(1, 'rgba(78,115,223,0.02)');
|
||||
return grad;
|
||||
}
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Vehicle speed',
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
fill: false,
|
||||
label: 'Speed (km/h)',
|
||||
data: [],
|
||||
borderColor: '#4e73df',
|
||||
backgroundColor: makeGradient(),
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4,
|
||||
pointHoverBackgroundColor: '#4e73df',
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: { duration: 200 },
|
||||
interaction: { mode: 'index', intersect: false },
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function (ctx) {
|
||||
return ctx.parsed.y != null ? ctx.parsed.y + ' km/h' : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
reverse: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Time',
|
||||
grid: { display: false },
|
||||
ticks: {
|
||||
maxTicksLimit: 6,
|
||||
color: '#adb5bd',
|
||||
font: { size: 10 },
|
||||
maxRotation: 0,
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Speed'
|
||||
beginAtZero: true,
|
||||
suggestedMax: 60,
|
||||
grid: { color: 'rgba(0,0,0,.05)' },
|
||||
ticks: {
|
||||
color: '#adb5bd',
|
||||
font: { size: 10 },
|
||||
callback: function (v) { return v + ' km/h'; },
|
||||
maxTicksLimit: 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// websockets
|
||||
// Session stats
|
||||
let sessionMaxSpeed = 0;
|
||||
let sessionSpeedSum = 0;
|
||||
let sessionSpeedCount = 0;
|
||||
|
||||
socket.on('connect', function(data) {
|
||||
console.log('socket.io is connected')
|
||||
function updateSpeedStats(speed) {
|
||||
if (speed == null || typeof speed !== 'number') return;
|
||||
|
||||
// Update session max
|
||||
if (speed > sessionMaxSpeed) {
|
||||
sessionMaxSpeed = speed;
|
||||
const maxEl = document.getElementById('speed-max-badge');
|
||||
if (maxEl) maxEl.textContent = speed.toFixed(0) + ' km/h';
|
||||
}
|
||||
|
||||
// Update session avg
|
||||
sessionSpeedSum += speed;
|
||||
sessionSpeedCount++;
|
||||
const avgEl = document.getElementById('speed-avg-badge');
|
||||
if (avgEl) avgEl.textContent = (sessionSpeedSum / sessionSpeedCount).toFixed(1) + ' km/h';
|
||||
|
||||
// Points count
|
||||
const ptEl = document.getElementById('speed-points-badge');
|
||||
if (ptEl) ptEl.textContent = sessionSpeedCount;
|
||||
}
|
||||
|
||||
// ── WebSocket events ──────────────────────────────────────────────────────
|
||||
|
||||
socket.on('connect', function () {
|
||||
console.log('socket.io connected');
|
||||
});
|
||||
|
||||
socket.on('obd_status', function(data) {
|
||||
console.log(data);
|
||||
const status = document.getElementById('obd-status');
|
||||
status.innerHTML = data;
|
||||
socket.on('obd_status', function (data) {
|
||||
const el = document.getElementById('obd-status');
|
||||
if (el) el.innerHTML = data;
|
||||
});
|
||||
|
||||
socket.on('obd_speed', function(data) {
|
||||
const status = document.getElementById('obd-speed');
|
||||
socket.on('obd_speed', function (data) {
|
||||
const statusEl = document.getElementById('obd-speed');
|
||||
const nowBadge = document.getElementById('speed-now-badge');
|
||||
const lastTime = document.getElementById('speed-last-time');
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
status.textContent = data;
|
||||
if (statusEl) statusEl.textContent = data;
|
||||
if (nowBadge) nowBadge.textContent = '—';
|
||||
return;
|
||||
}
|
||||
|
||||
const time = data[0]; // Assuming data[0] is the time value
|
||||
const speed = data[1]; // Assuming data[1] is the speed value
|
||||
const maxDataPoints = 8; // Adjust this value as needed
|
||||
const time = data[0]; // "HH:MM:SS"
|
||||
const speed = data[1]; // number, km/h
|
||||
|
||||
status.innerHTML = speed;
|
||||
// Update stat cards and badges
|
||||
if (statusEl) statusEl.textContent = speed != null ? speed + ' km/h' : '—';
|
||||
if (nowBadge) nowBadge.textContent = speed != null ? speed + ' km/h' : '—';
|
||||
if (lastTime) lastTime.textContent = 'Last: ' + time;
|
||||
|
||||
// Get a reference to the chart's data
|
||||
const chartData = chart.data.datasets[0].data;
|
||||
updateSpeedStats(speed);
|
||||
|
||||
// Unshift the new data points to the beginning of the chart dataset
|
||||
chartData.unshift({ x: time, y: speed });
|
||||
// Push into chart (oldest at left, newest at right)
|
||||
const labels = chart.data.labels;
|
||||
const points = chart.data.datasets[0].data;
|
||||
|
||||
// Remove data points if the number of data points exceeds maxDataPoints
|
||||
while (chartData.length > maxDataPoints) {
|
||||
chartData.pop(); // Remove the oldest data point from the end
|
||||
labels.push(time);
|
||||
points.push(speed);
|
||||
|
||||
if (labels.length > MAX_SPEED_POINTS) {
|
||||
labels.shift();
|
||||
points.shift();
|
||||
}
|
||||
|
||||
// Update the chart
|
||||
chart.update();
|
||||
// Refresh gradient in case canvas resized
|
||||
chart.data.datasets[0].backgroundColor = makeGradient();
|
||||
|
||||
// Dynamic Y max: 20% headroom above session max, min 60
|
||||
const displayMax = Math.max(60, Math.ceil((sessionMaxSpeed * 1.2) / 10) * 10);
|
||||
chart.options.scales.y.suggestedMax = displayMax;
|
||||
|
||||
chart.update('none'); // skip animation for live updates
|
||||
});
|
||||
|
||||
socket.on('obd_rpm', function(data) {
|
||||
console.log("obd rpm "+data);
|
||||
const status = document.getElementById('obd-rpm');
|
||||
status.innerHTML = data;
|
||||
socket.on('obd_rpm', function (data) {
|
||||
const el = document.getElementById('obd-rpm');
|
||||
if (el) el.textContent = data != null ? data + ' rpm' : '—';
|
||||
});
|
||||
|
||||
socket.on('obd_coolant_temp', function(data) {
|
||||
socket.on('obd_coolant_temp', function (data) {
|
||||
const el = document.getElementById('obd-coolant');
|
||||
if (el) el.textContent = data != null ? data + ' °C' : '—';
|
||||
});
|
||||
|
||||
socket.on('obd_fuel_level', function(data) {
|
||||
socket.on('obd_fuel_level', function (data) {
|
||||
const el = document.getElementById('obd-fuel');
|
||||
if (el) el.textContent = data != null ? data + ' %' : '—';
|
||||
});
|
||||
|
||||
socket.on('obd_throttle', function(data) {
|
||||
socket.on('obd_throttle', function (data) {
|
||||
const el = document.getElementById('obd-throttle');
|
||||
if (el) el.textContent = data != null ? data + ' %' : '—';
|
||||
});
|
||||
|
||||
socket.on('obd_error', function(data) {
|
||||
console.log("obd errors "+data);
|
||||
const status = document.getElementById('obd-error');
|
||||
if (status) {
|
||||
status.innerHTML = data;
|
||||
}
|
||||
socket.on('obd_error', function (data) {
|
||||
const el = document.getElementById('obd-error');
|
||||
if (el) el.innerHTML = data;
|
||||
});
|
||||
|
||||
let mapCentered = false;
|
||||
|
||||
Reference in New Issue
Block a user