// Velocity Chart Renderer // Renders line charts for velocity data comparison (OdomVel vs CmdVel) window.velocityChartRenderer = { linearChartCtx: null, angularChartCtx: null, linearChartData: { odom: [], cmd: [], times: [] }, angularChartData: { odom: [], cmd: [], times: [] }, initLinearVelocityChart: function(canvas) { if (!canvas || typeof canvas.getContext !== 'function') return; try { this.linearChartCtx = canvas.getContext('2d'); this.linearChartData = { odom: [], cmd: [], times: [] }; } catch (e) { console.error('Error initializing linear velocity chart:', e); } }, initAngularVelocityChart: function(canvas) { if (!canvas || typeof canvas.getContext !== 'function') return; try { this.angularChartCtx = canvas.getContext('2d'); this.angularChartData = { odom: [], cmd: [], times: [] }; } catch (e) { console.error('Error initializing angular velocity chart:', e); } }, updateLinearVelocityChart: function(canvas, data) { if (!canvas || !data || data.length === 0) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const width = canvas.width = canvas.offsetWidth; const height = canvas.height = canvas.offsetHeight || 150; // Clear canvas ctx.clearRect(0, 0, width, height); if (data.length === 0) return; // Calculate min/max values for scaling let dataMinVal = Math.min(...data.map(d => Math.min(d.odomVel, d.cmdVel))); let dataMaxVal = Math.max(...data.map(d => Math.max(d.odomVel, d.cmdVel))); // Set grid spacing to 0.1 m/s const gridStep = 0.1; // Round min/max to nearest 0.1 and add padding let minVal = Math.floor(dataMinVal / gridStep) * gridStep; let maxVal = Math.ceil(dataMaxVal / gridStep) * gridStep; // Ensure at least some range if (minVal === maxVal) { minVal -= gridStep; maxVal += gridStep; } // Add extra padding minVal -= gridStep; maxVal += gridStep; // Calculate time range const maxTime = Math.max(...data.map(d => d.time)); const minTime = Math.min(...data.map(d => d.time)); const timeRange = maxTime - minTime || 1; // Padding const padding = { top: 20, right: 20, bottom: 30, left: 50 }; const chartWidth = width - padding.left - padding.right; const chartHeight = height - padding.top - padding.bottom; // Draw axes ctx.strokeStyle = '#666'; ctx.lineWidth = 1; // X axis ctx.beginPath(); ctx.moveTo(padding.left, height - padding.bottom); ctx.lineTo(width - padding.right, height - padding.bottom); ctx.stroke(); // Y axis ctx.beginPath(); ctx.moveTo(padding.left, padding.top); ctx.lineTo(padding.left, height - padding.bottom); ctx.stroke(); // Draw grid lines with 0.1 m/s spacing ctx.strokeStyle = '#ddd'; ctx.lineWidth = 0.5; // Horizontal grid lines at 0.1 m/s intervals const valueRange = maxVal - minVal; const numSteps = Math.round(valueRange / gridStep); // Determine label step based on range to avoid overcrowding // If range is large, show labels less frequently let labelStep = 1; // Show every label by default if (valueRange > 1.0) { labelStep = 2; // Show every 2nd label if range > 1.0 } if (valueRange > 2.0) { labelStep = 5; // Show every 5th label if range > 2.0 } if (valueRange > 5.0) { labelStep = 10; // Show every 10th label if range > 5.0 } for (let i = 0; i <= numSteps; i++) { const value = minVal + i * gridStep; const y = padding.top + chartHeight - ((value - minVal) / valueRange) * chartHeight; ctx.beginPath(); ctx.moveTo(padding.left, y); ctx.lineTo(width - padding.right, y); ctx.stroke(); // Y axis labels - only show if it's a multiple of labelStep if (i % labelStep === 0) { ctx.fillStyle = '#666'; ctx.font = '10px Arial'; ctx.textAlign = 'right'; ctx.fillText(value.toFixed(2), padding.left - 5, y + 3); } } // Vertical grid lines (time) const xSteps = 5; for (let i = 0; i <= xSteps; i++) { const x = padding.left + (chartWidth / xSteps) * i; ctx.beginPath(); ctx.moveTo(x, padding.top); ctx.lineTo(x, height - padding.bottom); ctx.stroke(); // X axis labels const time = minTime + (timeRange / xSteps) * i; ctx.fillStyle = '#666'; ctx.font = '10px Arial'; ctx.textAlign = 'center'; ctx.fillText(time.toFixed(1) + 's', x, height - padding.bottom + 15); } // Draw OdomVel line (blue - #2196f3) if (data.length >= 1) { ctx.strokeStyle = '#2196f3'; // Blue ctx.lineWidth = 2; if (data.length > 1) { ctx.beginPath(); for (let i = 0; i < data.length; i++) { const x = padding.left + ((data[i].time - minTime) / timeRange) * chartWidth; const y = padding.top + chartHeight - ((data[i].odomVel - minVal) / (maxVal - minVal)) * chartHeight; if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } } ctx.stroke(); } // Draw point at current position const lastX = padding.left + ((data[data.length - 1].time - minTime) / timeRange) * chartWidth; const lastY = padding.top + chartHeight - ((data[data.length - 1].odomVel - minVal) / (maxVal - minVal)) * chartHeight; ctx.fillStyle = '#2196f3'; ctx.beginPath(); ctx.arc(lastX, lastY, 3, 0, 2 * Math.PI); ctx.fill(); } // Draw CmdVel line (green - #4caf50) if (data.length >= 1) { ctx.strokeStyle = '#4caf50'; // Green ctx.lineWidth = 2; if (data.length > 1) { ctx.beginPath(); for (let i = 0; i < data.length; i++) { const x = padding.left + ((data[i].time - minTime) / timeRange) * chartWidth; const y = padding.top + chartHeight - ((data[i].cmdVel - minVal) / (maxVal - minVal)) * chartHeight; if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } } ctx.stroke(); } // Draw point at current position const lastX = padding.left + ((data[data.length - 1].time - minTime) / timeRange) * chartWidth; const lastY = padding.top + chartHeight - ((data[data.length - 1].cmdVel - minVal) / (maxVal - minVal)) * chartHeight; ctx.fillStyle = '#4caf50'; ctx.beginPath(); ctx.arc(lastX, lastY, 3, 0, 2 * Math.PI); ctx.fill(); } }, updateAngularVelocityChart: function(canvas, data) { if (!canvas || !data || data.length === 0) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const width = canvas.width = canvas.offsetWidth; const height = canvas.height = canvas.offsetHeight || 150; // Clear canvas ctx.clearRect(0, 0, width, height); if (data.length === 0) return; // Calculate min/max values for scaling let dataMinVal = Math.min(...data.map(d => Math.min(d.odomVel, d.cmdVel))); let dataMaxVal = Math.max(...data.map(d => Math.max(d.odomVel, d.cmdVel))); // Set grid spacing to 0.1 rad/s const gridStep = 0.1; // Round min/max to nearest 0.1 and add padding let minVal = Math.floor(dataMinVal / gridStep) * gridStep; let maxVal = Math.ceil(dataMaxVal / gridStep) * gridStep; // Ensure at least some range if (minVal === maxVal) { minVal -= gridStep; maxVal += gridStep; } // Add extra padding minVal -= gridStep; maxVal += gridStep; // Calculate time range const maxTime = Math.max(...data.map(d => d.time)); const minTime = Math.min(...data.map(d => d.time)); const timeRange = maxTime - minTime || 1; // Padding const padding = { top: 20, right: 20, bottom: 30, left: 50 }; const chartWidth = width - padding.left - padding.right; const chartHeight = height - padding.top - padding.bottom; // Draw axes ctx.strokeStyle = '#666'; ctx.lineWidth = 1; // X axis ctx.beginPath(); ctx.moveTo(padding.left, height - padding.bottom); ctx.lineTo(width - padding.right, height - padding.bottom); ctx.stroke(); // Y axis ctx.beginPath(); ctx.moveTo(padding.left, padding.top); ctx.lineTo(padding.left, height - padding.bottom); ctx.stroke(); // Draw grid lines with 0.1 rad/s spacing ctx.strokeStyle = '#ddd'; ctx.lineWidth = 0.5; // Horizontal grid lines at 0.1 rad/s intervals const valueRange = maxVal - minVal; const numSteps = Math.round(valueRange / gridStep); // Determine label step based on range to avoid overcrowding // If range is large, show labels less frequently let labelStep = 1; // Show every label by default if (valueRange > 1.0) { labelStep = 2; // Show every 2nd label if range > 1.0 } if (valueRange > 2.0) { labelStep = 5; // Show every 5th label if range > 2.0 } if (valueRange > 5.0) { labelStep = 10; // Show every 10th label if range > 5.0 } for (let i = 0; i <= numSteps; i++) { const value = minVal + i * gridStep; const y = padding.top + chartHeight - ((value - minVal) / valueRange) * chartHeight; ctx.beginPath(); ctx.moveTo(padding.left, y); ctx.lineTo(width - padding.right, y); ctx.stroke(); // Y axis labels - only show if it's a multiple of labelStep if (i % labelStep === 0) { ctx.fillStyle = '#666'; ctx.font = '10px Arial'; ctx.textAlign = 'right'; ctx.fillText(value.toFixed(1), padding.left - 5, y + 3); } } // Vertical grid lines (time) const xSteps = 5; for (let i = 0; i <= xSteps; i++) { const x = padding.left + (chartWidth / xSteps) * i; ctx.beginPath(); ctx.moveTo(x, padding.top); ctx.lineTo(x, height - padding.bottom); ctx.stroke(); // X axis labels const time = minTime + (timeRange / xSteps) * i; ctx.fillStyle = '#666'; ctx.font = '10px Arial'; ctx.textAlign = 'center'; ctx.fillText(time.toFixed(1) + 's', x, height - padding.bottom + 15); } // Draw OdomVel line (blue - #2196f3) if (data.length >= 1) { ctx.strokeStyle = '#2196f3'; // Blue ctx.lineWidth = 2; if (data.length > 1) { ctx.beginPath(); for (let i = 0; i < data.length; i++) { const x = padding.left + ((data[i].time - minTime) / timeRange) * chartWidth; const y = padding.top + chartHeight - ((data[i].odomVel - minVal) / (maxVal - minVal)) * chartHeight; if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } } ctx.stroke(); } // Draw point at current position const lastX = padding.left + ((data[data.length - 1].time - minTime) / timeRange) * chartWidth; const lastY = padding.top + chartHeight - ((data[data.length - 1].odomVel - minVal) / (maxVal - minVal)) * chartHeight; ctx.fillStyle = '#2196f3'; ctx.beginPath(); ctx.arc(lastX, lastY, 3, 0, 2 * Math.PI); ctx.fill(); } // Draw CmdVel line (green - #4caf50) if (data.length >= 1) { ctx.strokeStyle = '#4caf50'; // Green ctx.lineWidth = 2; if (data.length > 1) { ctx.beginPath(); for (let i = 0; i < data.length; i++) { const x = padding.left + ((data[i].time - minTime) / timeRange) * chartWidth; const y = padding.top + chartHeight - ((data[i].cmdVel - minVal) / (maxVal - minVal)) * chartHeight; if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } } ctx.stroke(); } // Draw point at current position const lastX = padding.left + ((data[data.length - 1].time - minTime) / timeRange) * chartWidth; const lastY = padding.top + chartHeight - ((data[data.length - 1].cmdVel - minVal) / (maxVal - minVal)) * chartHeight; ctx.fillStyle = '#4caf50'; ctx.beginPath(); ctx.arc(lastX, lastY, 3, 0, 2 * Math.PI); ctx.fill(); } } };