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:
+126
-59
@@ -1,120 +1,187 @@
|
|||||||
const socket = io();
|
const socket = io();
|
||||||
// map
|
|
||||||
|
|
||||||
// Offline tiles pre-downloaded via scripts/download_map_tiles.py (see PLAN.md
|
// ── Map ────────────────────────────────────────────────────────────────────
|
||||||
// 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).
|
|
||||||
var map = L.map('map', { minZoom: 8, maxZoom: 15 }).setView([48.505, -0.09], 12);
|
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', {
|
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',
|
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,
|
minZoom: 8,
|
||||||
maxZoom: 15
|
maxZoom: 15,
|
||||||
});
|
});
|
||||||
var carMarker = L.marker(map.getCenter()).addTo(map)
|
var carMarker = L.marker(map.getCenter()).addTo(map).bindPopup('Your Yaris');
|
||||||
.bindPopup('your car... soon(tm)');
|
|
||||||
|
|
||||||
// charts
|
// ── Speed history chart ────────────────────────────────────────────────────
|
||||||
|
const MAX_SPEED_POINTS = 60; // ~5 min at typical OBD poll rate
|
||||||
|
|
||||||
const ctx = document.getElementById('speedGraph').getContext('2d');
|
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',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
|
labels: [],
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Vehicle speed',
|
label: 'Speed (km/h)',
|
||||||
borderColor: 'rgba(75, 192, 192, 1)',
|
data: [],
|
||||||
fill: false,
|
borderColor: '#4e73df',
|
||||||
|
backgroundColor: makeGradient(),
|
||||||
|
borderWidth: 2,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHoverRadius: 4,
|
||||||
|
pointHoverBackgroundColor: '#4e73df',
|
||||||
|
tension: 0.4,
|
||||||
|
fill: true,
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
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: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
reverse: true,
|
grid: { display: false },
|
||||||
title: {
|
ticks: {
|
||||||
display: true,
|
maxTicksLimit: 6,
|
||||||
text: 'Time',
|
color: '#adb5bd',
|
||||||
|
font: { size: 10 },
|
||||||
|
maxRotation: 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
title: {
|
beginAtZero: true,
|
||||||
display: true,
|
suggestedMax: 60,
|
||||||
text: 'Speed'
|
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) {
|
function updateSpeedStats(speed) {
|
||||||
console.log('socket.io is connected')
|
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) {
|
socket.on('obd_status', function (data) {
|
||||||
console.log(data);
|
const el = document.getElementById('obd-status');
|
||||||
const status = document.getElementById('obd-status');
|
if (el) el.innerHTML = data;
|
||||||
status.innerHTML = data;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('obd_speed', function(data) {
|
socket.on('obd_speed', function (data) {
|
||||||
const status = document.getElementById('obd-speed');
|
const statusEl = document.getElementById('obd-speed');
|
||||||
|
const nowBadge = document.getElementById('speed-now-badge');
|
||||||
|
const lastTime = document.getElementById('speed-last-time');
|
||||||
|
|
||||||
if (!Array.isArray(data)) {
|
if (!Array.isArray(data)) {
|
||||||
status.textContent = data;
|
if (statusEl) statusEl.textContent = data;
|
||||||
|
if (nowBadge) nowBadge.textContent = '—';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const time = data[0]; // Assuming data[0] is the time value
|
const time = data[0]; // "HH:MM:SS"
|
||||||
const speed = data[1]; // Assuming data[1] is the speed value
|
const speed = data[1]; // number, km/h
|
||||||
const maxDataPoints = 8; // Adjust this value as needed
|
|
||||||
|
|
||||||
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
|
updateSpeedStats(speed);
|
||||||
const chartData = chart.data.datasets[0].data;
|
|
||||||
|
|
||||||
// Unshift the new data points to the beginning of the chart dataset
|
// Push into chart (oldest at left, newest at right)
|
||||||
chartData.unshift({ x: time, y: speed });
|
const labels = chart.data.labels;
|
||||||
|
const points = chart.data.datasets[0].data;
|
||||||
|
|
||||||
// Remove data points if the number of data points exceeds maxDataPoints
|
labels.push(time);
|
||||||
while (chartData.length > maxDataPoints) {
|
points.push(speed);
|
||||||
chartData.pop(); // Remove the oldest data point from the end
|
|
||||||
|
if (labels.length > MAX_SPEED_POINTS) {
|
||||||
|
labels.shift();
|
||||||
|
points.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the chart
|
// Refresh gradient in case canvas resized
|
||||||
chart.update();
|
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) {
|
socket.on('obd_rpm', function (data) {
|
||||||
console.log("obd rpm "+data);
|
const el = document.getElementById('obd-rpm');
|
||||||
const status = document.getElementById('obd-rpm');
|
if (el) el.textContent = data != null ? data + ' rpm' : '—';
|
||||||
status.innerHTML = data;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('obd_coolant_temp', function(data) {
|
socket.on('obd_coolant_temp', function (data) {
|
||||||
const el = document.getElementById('obd-coolant');
|
const el = document.getElementById('obd-coolant');
|
||||||
if (el) el.textContent = data != null ? data + ' °C' : '—';
|
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');
|
const el = document.getElementById('obd-fuel');
|
||||||
if (el) el.textContent = data != null ? data + ' %' : '—';
|
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');
|
const el = document.getElementById('obd-throttle');
|
||||||
if (el) el.textContent = data != null ? data + ' %' : '—';
|
if (el) el.textContent = data != null ? data + ' %' : '—';
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('obd_error', function(data) {
|
socket.on('obd_error', function (data) {
|
||||||
console.log("obd errors "+data);
|
const el = document.getElementById('obd-error');
|
||||||
const status = document.getElementById('obd-error');
|
if (el) el.innerHTML = data;
|
||||||
if (status) {
|
|
||||||
status.innerHTML = data;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let mapCentered = false;
|
let mapCentered = false;
|
||||||
|
|||||||
+14
-6
@@ -115,18 +115,26 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-lg-7 col-xl-8">
|
<div class="col-lg-7 col-xl-8">
|
||||||
<div class="card shadow-sm h-100">
|
<div class="card shadow-sm h-100">
|
||||||
<div class="card-header py-2 d-flex align-items-center justify-content-between">
|
<div class="card-header py-2 d-flex align-items-center gap-3">
|
||||||
<h6 class="text-primary fw-bold m-0">Speed history</h6>
|
<h6 class="text-primary fw-bold m-0 me-auto">Speed history</h6>
|
||||||
<button class="btn btn-link btn-sm p-0 text-muted" id="speed-graph-toggle" type="button"
|
{# Live speed badge — visible even when graph is collapsed #}
|
||||||
|
<span class="text-xs text-muted fw-bold text-uppercase">Now</span>
|
||||||
|
<span id="speed-now-badge" class="fw-bold text-dark" style="min-width:3.5rem;text-align:right;">—</span>
|
||||||
|
<span class="text-xs text-muted fw-bold text-uppercase ms-2">Max</span>
|
||||||
|
<span id="speed-max-badge" class="fw-bold text-danger" style="min-width:3.5rem;text-align:right;">—</span>
|
||||||
|
<button class="btn btn-link btn-sm p-0 text-muted ms-2" id="speed-graph-toggle" type="button"
|
||||||
aria-expanded="false" aria-controls="speed-graph-body">
|
aria-expanded="false" aria-controls="speed-graph-body">
|
||||||
<i class="fas fa-chevron-down"></i>
|
<i class="fas fa-chevron-down"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse" id="speed-graph-body">
|
<div class="collapse" id="speed-graph-body">
|
||||||
<div class="card-body">
|
<div class="card-body pb-2 pt-3 px-3">
|
||||||
<div class="chart-container" style="position:relative;height:14rem;">
|
<canvas id="speedGraph" style="height:13rem;"></canvas>
|
||||||
<canvas id="speedGraph"></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-footer py-2 d-flex gap-4 bg-white border-top small text-muted">
|
||||||
|
<div><span class="fw-bold text-uppercase" style="font-size:.65rem;">Avg (session)</span> <span id="speed-avg-badge" class="text-dark fw-semibold">—</span></div>
|
||||||
|
<div><span class="fw-bold text-uppercase" style="font-size:.65rem;">Points</span> <span id="speed-points-badge" class="text-dark fw-semibold">0</span></div>
|
||||||
|
<div class="ms-auto"><span id="speed-last-time" class="text-muted" style="font-size:.7rem;"></span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user