33class 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