Skip to content

Commit 4d81dfb

Browse files
Merge pull request #1 from workoutmachineappfree/improved-graph
Improved Graph
2 parents f45db80 + f4e3831 commit 4d81dfb

File tree

3 files changed

+509
-198
lines changed

3 files changed

+509
-198
lines changed

app.js

Lines changed: 16 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
class VitruvianApp {
44
constructor() {
55
this.device = new VitruvianDevice();
6-
this.loadHistory = [];
7-
this.maxHistoryPoints = 300; // 30 seconds at 100ms polling
6+
this.chartManager = new ChartManager("loadGraph");
87
this.maxPosA = 1000; // Dynamic max for Right Cable (A)
98
this.maxPosB = 1000; // Dynamic max for Left Cable (B)
109
this.warmupReps = 0;
@@ -30,7 +29,7 @@ class VitruvianApp {
3029
this.isJustLiftMode = false; // Flag for Just Lift mode with auto-stop
3130
this.lastTopCounter = undefined; // Track u16[1] for top detection
3231
this.setupLogging();
33-
this.setupGraph();
32+
this.setupChart();
3433
this.resetRepCountersToEmpty();
3534
}
3635

@@ -41,185 +40,12 @@ class VitruvianApp {
4140
};
4241
}
4342

44-
setupGraph() {
45-
// Initialize canvas for load graph
46-
this.canvas = document.getElementById("loadGraph");
47-
if (!this.canvas) {
48-
console.warn("Canvas element not found yet, will initialize later");
49-
return;
50-
}
51-
52-
// Get parent container width to size canvas responsively
53-
const container = this.canvas.parentElement;
54-
const rect = container.getBoundingClientRect();
55-
const width = Math.max(rect.width, 400); // Min 400px width
56-
const height = 300;
57-
58-
// Apply devicePixelRatio for crisp rendering on high-DPI displays
59-
const dpr = window.devicePixelRatio || 1;
60-
61-
// Set canvas resolution (physical pixels)
62-
this.canvas.width = width * dpr;
63-
this.canvas.height = height * dpr;
64-
65-
// Set display size (CSS pixels) to fill container
66-
this.canvas.style.width = "100%";
67-
this.canvas.style.height = height + "px";
68-
69-
// Store logical dimensions for drawing
70-
this.canvasDisplayWidth = width;
71-
this.canvasDisplayHeight = height;
72-
73-
this.ctx = this.canvas.getContext("2d", { alpha: false });
74-
75-
// Scale context to match DPI
76-
this.ctx.scale(dpr, dpr);
77-
78-
// Disable smoothing for crisp rendering
79-
this.ctx.imageSmoothingEnabled = false;
80-
81-
this.drawGraph();
82-
}
83-
84-
drawGraph() {
85-
if (!this.ctx || !this.canvas) return;
86-
87-
const width = this.canvasDisplayWidth || this.canvas.width;
88-
const height = this.canvasDisplayHeight || this.canvas.height;
89-
const ctx = this.ctx;
90-
91-
if (width === 0 || height === 0) return; // Canvas not sized yet
92-
93-
// Clear canvas
94-
ctx.fillStyle = "#f8f9fa";
95-
ctx.fillRect(0, 0, width, height);
96-
97-
// Set text rendering for crisp fonts
98-
ctx.textRendering = "optimizeLegibility";
99-
100-
if (this.loadHistory.length < 2) {
101-
// Not enough data to draw
102-
ctx.fillStyle = "#6c757d";
103-
ctx.font = "14px -apple-system, sans-serif";
104-
ctx.textAlign = "center";
105-
ctx.fillText(
106-
"Waiting for data...",
107-
Math.round(width / 2),
108-
Math.round(height / 2),
109-
);
110-
return;
111-
}
112-
113-
// Find max load for scaling
114-
let maxLoad = 0;
115-
for (const point of this.loadHistory) {
116-
const totalLoad = point.loadA + point.loadB;
117-
if (totalLoad > maxLoad) maxLoad = totalLoad;
118-
}
119-
120-
// Add some headroom
121-
maxLoad = maxLoad * 1.2 || 10;
122-
123-
const padding = 40;
124-
const graphWidth = width - padding * 2;
125-
const graphHeight = height - padding * 2;
126-
127-
// Draw horizontal grid lines
128-
ctx.strokeStyle = "#dee2e6";
129-
ctx.lineWidth = 1;
130-
for (let i = 0; i <= 5; i++) {
131-
const y = Math.round(padding + (graphHeight / 5) * i) + 0.5; // +0.5 for crisp 1px lines
132-
ctx.beginPath();
133-
ctx.moveTo(padding, y);
134-
ctx.lineTo(width - padding, y);
135-
ctx.stroke();
136-
137-
// Draw Y-axis labels (load)
138-
const loadValue = maxLoad * (1 - i / 5);
139-
ctx.fillStyle = "#6c757d";
140-
ctx.font = "11px -apple-system, sans-serif";
141-
ctx.textAlign = "right";
142-
ctx.fillText(
143-
loadValue.toFixed(1) + " kg",
144-
padding - 5,
145-
Math.round(y + 4),
146-
);
147-
}
148-
149-
// Draw X-axis time label (t-0)
150-
const timeLabels = [{ label: "t-0", position: 1 }];
151-
152-
ctx.fillStyle = "#6c757d";
153-
ctx.font = "11px -apple-system, sans-serif";
154-
ctx.textAlign = "center";
155-
156-
timeLabels.forEach((item) => {
157-
const x = padding + graphWidth * item.position;
158-
ctx.fillText(item.label, Math.round(x), height - padding + 15);
159-
});
160-
161-
// Draw lines for each cable and total
162-
// Calculate spacing based on actual number of points to fill the graph width
163-
const numPoints = this.loadHistory.length;
164-
const pointSpacing = numPoints > 1 ? graphWidth / (numPoints - 1) : 0;
165-
166-
// Helper to draw a line
167-
const drawLine = (getData, color, lineWidth = 2) => {
168-
ctx.strokeStyle = color;
169-
ctx.lineWidth = lineWidth;
170-
ctx.lineJoin = "round";
171-
ctx.lineCap = "round";
172-
ctx.beginPath();
173-
174-
let started = false;
175-
for (let i = 0; i < this.loadHistory.length; i++) {
176-
const point = this.loadHistory[i];
177-
const value = getData(point);
178-
const x = padding + i * pointSpacing;
179-
const y = padding + graphHeight - (value / maxLoad) * graphHeight;
180-
181-
if (!started) {
182-
ctx.moveTo(x, y);
183-
started = true;
184-
} else {
185-
ctx.lineTo(x, y);
186-
}
187-
}
188-
ctx.stroke();
43+
setupChart() {
44+
// Initialize chart and connect logging
45+
this.chartManager.init();
46+
this.chartManager.onLog = (message, type) => {
47+
this.addLogEntry(message, type);
18948
};
190-
191-
// Draw total load (thicker line)
192-
drawLine((p) => p.loadA + p.loadB, "#667eea", 3);
193-
194-
// Draw individual cables (thinner lines)
195-
drawLine((p) => p.loadA, "#51cf66", 1.5);
196-
drawLine((p) => p.loadB, "#ff6b6b", 1.5);
197-
198-
// Draw compact horizontal legend at top
199-
const legendItems = [
200-
{ label: "Total", color: "#667eea" },
201-
{ label: "Right", color: "#51cf66" },
202-
{ label: "Left", color: "#ff6b6b" },
203-
];
204-
205-
ctx.font = "11px -apple-system, sans-serif";
206-
ctx.textAlign = "left";
207-
208-
let legendX = padding + 10;
209-
const legendY = padding - 20;
210-
211-
legendItems.forEach((item, i) => {
212-
// Draw color box
213-
ctx.fillStyle = item.color;
214-
ctx.fillRect(Math.round(legendX), Math.round(legendY - 6), 10, 8);
215-
216-
// Draw label
217-
ctx.fillStyle = "#495057";
218-
ctx.fillText(item.label, Math.round(legendX + 13), Math.round(legendY));
219-
220-
// Move to next position (compact spacing)
221-
legendX += ctx.measureText(item.label).width + 28;
222-
});
22349
}
22450

22551
addLogEntry(message, type = "info") {
@@ -306,20 +132,17 @@ class VitruvianApp {
306132
this.checkAutoStop(sample);
307133
}
308134

309-
// Add to load history
310-
this.loadHistory.push({
311-
timestamp: sample.timestamp,
312-
loadA: sample.loadA,
313-
loadB: sample.loadB,
314-
});
135+
// Add data to chart
136+
this.chartManager.addData(sample);
137+
}
315138

316-
// Trim history to max points
317-
if (this.loadHistory.length > this.maxHistoryPoints) {
318-
this.loadHistory.shift();
319-
}
139+
// Delegate chart methods to ChartManager
140+
setTimeRange(seconds) {
141+
this.chartManager.setTimeRange(seconds);
142+
}
320143

321-
// Redraw graph
322-
this.drawGraph();
144+
exportData() {
145+
this.chartManager.exportCSV();
323146
}
324147

325148
// Mobile sidebar toggle

0 commit comments

Comments
 (0)